From 1a929e3e249ffe56018dc8b5b6b320df173a46c1 Mon Sep 17 00:00:00 2001
From: "SREDevOps.org Agent"
Date: Sat, 31 Oct 2026 00:57:35 -0300
Subject: [PATCH 1/3] fix(gateway)!: replace token cookies with revocable
sessions and harden abuse controls (#40)
- Replace user:token cookies with opaque server-side sessions, revocable on logout, delete, recreate and expiry; bind every credential to the path user and add cross-user regression tests.
- Rate-limit /users and /login, add optional signup invite codes and form same-origin checks, and unify login failures to 401.
- Require explicit ALLOW_INSECURE_COOKIES for plain HTTP, use __Host-dsh_gw_session when secure, and fix secure cookie deletion.
- Strip client-supplied forwarding/SSO headers, add an optional upstream allowlist, per-user in-flight and WebSocket message caps, credential-free handshake errors, and sweep-time state pruning.
- Digest-pin base images, hash-pin gateway dependencies, verify DSH_COMMIT in the build, and run the gateway image as non-root with a pinned Python base.
- Update manifests, production checklist, architecture/security docs and add an SSO preflight script.
BREAKING CHANGE: existing dsh_gw_session=: cookies are invalid; users must sign in again. Unknown users on POST /login now return 401 instead of 404, and the gateway refuses COOKIE_SECURE=false unless ALLOW_INSECURE_COOKIES=1.
Co-authored-by: SREDevOpsAgents
---
.env.sample | 3 +
.github/workflows/build-main.yaml | 3 +
.github/workflows/ci-image.yaml | 2 +
.github/workflows/ci-python.yaml | 4 +-
.github/workflows/release.yaml | 21 +-
AGENTS.md | 28 +-
Dockerfile | 20 +-
Makefile | 1 +
SECURITY.md | 38 +-
docs/main-image.md | 21 +-
docs/versioning.md | 42 +-
examples/dsh-agents-as-a-service/README.md | 215 +-
.../docs/architecture.md | 134 +-
.../docs/optional-hardening.md | 124 +-
.../gateway/Dockerfile | 67 +-
.../dsh-agents-as-a-service/gateway/README.md | 115 +-
.../gateway/gateway.py | 395 +++-
.../gateway/requirements-dev.txt | 1745 ++++++++++++++++-
.../gateway/requirements.txt | 1587 ++++++++++++++-
.../gateway/test_gateway.py | 680 ++++++-
.../manifests/10-sandboxtemplate.yaml | 18 +
.../manifests/50-gateway.yaml | 68 +-
.../manifests/exposure/README.md | 43 +-
.../gateway-api-sso/72-oauth2-proxy.yaml | 7 +-
.../gateway-api-sso/75-networkpolicies.yaml | 9 +-
.../exposure/ingress/60-ingress.yaml | 17 +-
.../exposure/ingress/61-networkpolicy.yaml | 9 +-
.../manifests/optional/README.md | 6 +
.../10-sandbox-networkpolicy.yaml | 8 +-
.../scripts/preflight-sso.sh | 234 +++
scripts/check-versions.sh | 38 +
scripts/set-version.sh | 51 +-
versions.env | 16 +-
33 files changed, 5450 insertions(+), 319 deletions(-)
create mode 100755 examples/dsh-agents-as-a-service/scripts/preflight-sso.sh
diff --git a/.env.sample b/.env.sample
index 289c792..c74e00c 100644
--- a/.env.sample
+++ b/.env.sample
@@ -23,6 +23,9 @@ CEREBRAS_API_KEY=
AI_GATEWAY_API_KEY=
HF_TOKEN=
DSH_VERSION=dsh-v0.2.1-rc.5
+# Full upstream commit SHA that DSH_VERSION resolves to; the main image build
+# fails if the clone's HEAD differs (guards against a moved tag).
+DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1
RELEASE_REVISION=
PNPM_VERSION=11.28.5
# Container port, and the host port published by "make run".
diff --git a/.github/workflows/build-main.yaml b/.github/workflows/build-main.yaml
index b70fedb..28c8f33 100644
--- a/.github/workflows/build-main.yaml
+++ b/.github/workflows/build-main.yaml
@@ -29,6 +29,7 @@ jobs:
outputs:
identity: ${{ steps.meta.outputs.identity }}
dsh_version: ${{ steps.meta.outputs.dsh_version }}
+ dsh_commit: ${{ steps.meta.outputs.dsh_commit }}
pnpm_version: ${{ steps.meta.outputs.pnpm_version }}
release_revision: ${{ steps.meta.outputs.release_revision }}
ghcr_image: ${{ steps.meta.outputs.ghcr_image }}
@@ -53,6 +54,7 @@ jobs:
{
echo "identity=$identity"
echo "dsh_version=$DSH_VERSION"
+ echo "dsh_commit=$DSH_COMMIT"
echo "pnpm_version=$PNPM_VERSION"
echo "release_revision=$RELEASE_REVISION"
echo "ghcr_image=$GHCR_IMAGE"
@@ -75,6 +77,7 @@ jobs:
dockerfile: Dockerfile
build-args: |
DSH_VERSION=${{ needs.meta.outputs.dsh_version }}
+ DSH_COMMIT=${{ needs.meta.outputs.dsh_commit }}
RELEASE_REVISION=${{ needs.meta.outputs.release_revision }}
PNPM_VERSION=${{ needs.meta.outputs.pnpm_version }}
VCS_REF=${{ github.sha }}
diff --git a/.github/workflows/ci-image.yaml b/.github/workflows/ci-image.yaml
index ded8dea..294f080 100644
--- a/.github/workflows/ci-image.yaml
+++ b/.github/workflows/ci-image.yaml
@@ -48,6 +48,7 @@ jobs:
{
echo "identity=$identity"
echo "dsh_version=$DSH_VERSION"
+ echo "dsh_commit=$DSH_COMMIT"
echo "pnpm_version=$PNPM_VERSION"
echo "release_revision=${RELEASE_REVISION:-}"
} >> "$GITHUB_OUTPUT"
@@ -66,6 +67,7 @@ jobs:
tags: dsh-web:ci
build-args: |
DSH_VERSION=${{ steps.meta.outputs.dsh_version }}
+ DSH_COMMIT=${{ steps.meta.outputs.dsh_commit }}
RELEASE_REVISION=${{ steps.meta.outputs.release_revision }}
PNPM_VERSION=${{ steps.meta.outputs.pnpm_version }}
VCS_REF=${{ github.sha }}
diff --git a/.github/workflows/ci-python.yaml b/.github/workflows/ci-python.yaml
index 0a14248..7615b4f 100644
--- a/.github/workflows/ci-python.yaml
+++ b/.github/workflows/ci-python.yaml
@@ -43,7 +43,9 @@ jobs:
run: |
set -euo pipefail
python -m pip install --upgrade pip
- pip install -r requirements.txt -r requirements-dev.txt
+ # Both files are fully hash-pinned (uv pip compile --generate-hashes);
+ # pip refuses to install anything not listed with a matching hash.
+ pip install --require-hashes -r requirements.txt -r requirements-dev.txt
- name: Lint
run: |
set -euo pipefail
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 13fd068..6c69fc9 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -34,6 +34,7 @@ jobs:
outputs:
identity: ${{ steps.meta.outputs.identity }}
dsh_version: ${{ steps.meta.outputs.dsh_version }}
+ dsh_commit: ${{ steps.meta.outputs.dsh_commit }}
pnpm_version: ${{ steps.meta.outputs.pnpm_version }}
release_revision: ${{ steps.meta.outputs.release_revision }}
ghcr_image: ${{ steps.meta.outputs.ghcr_image }}
@@ -91,13 +92,28 @@ jobs:
echo "tag '$tag' does not exist in this repository" >&2
exit 1
fi
- if ! git ls-remote --tags --exit-code "$DSH_REPO" "$DSH_VERSION" >/dev/null; then
+ if ! upstream="$(git ls-remote --tags --exit-code "$DSH_REPO" \
+ "refs/tags/$DSH_VERSION^{}" "refs/tags/$DSH_VERSION")"; then
echo "upstream ref '$DSH_VERSION' not found in $DSH_REPO" >&2
exit 1
fi
+ # Never release a ref that no longer resolves to the pinned commit: an
+ # annotated tag has a peeled ^{} line, a lightweight tag only the ref.
+ resolved="$(printf '%s\n' "$upstream" \
+ | awk -v t="refs/tags/$DSH_VERSION^{}" '$2 == t {print $1}' | head -n1)"
+ if [ -z "$resolved" ]; then
+ resolved="$(printf '%s\n' "$upstream" \
+ | awk -v t="refs/tags/$DSH_VERSION" '$2 == t {print $1}' | head -n1)"
+ fi
+ if [ "$resolved" != "$DSH_COMMIT" ]; then
+ echo "upstream $DSH_REPO@$DSH_VERSION resolved to '$resolved'," \
+ "versions.env pins DSH_COMMIT=$DSH_COMMIT" >&2
+ exit 1
+ fi
{
echo "identity=$identity"
echo "dsh_version=$DSH_VERSION"
+ echo "dsh_commit=$DSH_COMMIT"
echo "pnpm_version=$PNPM_VERSION"
echo "release_revision=$RELEASE_REVISION"
echo "ghcr_image=$GHCR_IMAGE"
@@ -139,7 +155,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --upgrade pip
- pip install -r requirements.txt -r requirements-dev.txt
+ pip install --require-hashes -r requirements.txt -r requirements-dev.txt
- name: Lint
if: needs.meta.outputs.run_tests == 'true'
run: |
@@ -166,6 +182,7 @@ jobs:
dockerfile: Dockerfile
build-args: |
DSH_VERSION=${{ needs.meta.outputs.dsh_version }}
+ DSH_COMMIT=${{ needs.meta.outputs.dsh_commit }}
RELEASE_REVISION=${{ needs.meta.outputs.release_revision }}
PNPM_VERSION=${{ needs.meta.outputs.pnpm_version }}
VCS_REF=${{ github.sha }}
diff --git a/AGENTS.md b/AGENTS.md
index 26b0c62..7cb1ed9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -117,17 +117,33 @@ needs kind plus the agent-sandbox CRDs (see the example README).
- AUTH_PROXY_HEADER is opt-in. Only enable it when the gateway Service is
reachable exclusively through the authenticating proxy; otherwise any client
- can spoof X-Forwarded-User.
+ can spoof X-Forwarded-User. The SSO variant's NetworkPolicy is required for
+ production, and `examples/dsh-agents-as-a-service/scripts/preflight-sso.sh`
+ verifies the setup (Secrets, gateway env, TLS reference, placeholders, second
+ paths).
- The example gateway reads the DSH launch token from pod logs. Treat pod logs
as sensitive and keep its RBAC namespace-scoped.
+- Browser sessions are opaque server-side ids held in gateway memory: they are
+ revocable (logout/delete/recreate/expiry) but a gateway restart logs everyone
+ out and horizontal scaling needs a shared store. Session cookies are never
+ accepted as bearer tokens.
+- AUTH_RATE_LIMIT, SIGNUP_INVITE_CODE, MAX_IN_FLIGHT_PER_USER and
+ WS_MAX_MESSAGE_BYTES are in-process abuse guards, not ingress rate limits,
+ quotas or an audit log. Add those for an internet-facing deployment.
- The example shares provider keys (dsh-provider-keys plus the optional
dsh-provider-keys-extra) across sandboxes; do not describe it as
tenant-isolated.
-- gVisor, nodeSelector, the NetworkPolicies, extra provider keys and failed-Pod
- recovery are opt-in and are not active in the base manifests; do not describe
- the base example as hardened.
-- TLS terminates outside the gateway; set COOKIE_SECURE=true for HTTPS and keep
- the base manifest safe for local HTTP.
+- gVisor, nodeSelector, the shipped sandbox/exposure NetworkPolicies and the
+ documented gateway egress policy, extra provider keys and failed-Pod recovery
+ are opt-in and are not active in the base manifests; do not describe the base
+ example as hardened.
+ The sandbox and gateway-egress policies are required production steps; an
+ exposure without its NetworkPolicy is demo-only. See the example's
+ production checklist.
+- TLS terminates outside the gateway. Set COOKIE_SECURE=true for HTTPS and drop
+ ALLOW_INSECURE_COOKIES; the gateway refuses COOKIE_SECURE=false unless
+ ALLOW_INSECURE_COOKIES=1, which is only for the plain-HTTP local demo. Never
+ expose that combination.
## Ask before
diff --git a/Dockerfile b/Dockerfile
index 48733ac..a28e2d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,13 +6,14 @@
# Build args (all pinned in versions.env, verified by scripts/check-versions.sh):
# DSH_REPO upstream git remote to package
# DSH_VERSION upstream git ref to build (tag or branch)
+# DSH_COMMIT full commit SHA DSH_VERSION must resolve to
# RELEASE_REVISION optional repository revision label (r2, r3, ...)
# PNPM_VERSION pnpm used to build DSH
# VCS_REF this repository's commit, recorded in OCI labels
-# NODE_BUILD_IMAGE node image used to build DSH
-# NODE_RUNTIME_IMAGE node image used at runtime
-ARG NODE_BUILD_IMAGE=docker.io/node:24-trixie
-ARG NODE_RUNTIME_IMAGE=docker.io/node:24-trixie-slim
+# NODE_BUILD_IMAGE node image used to build DSH (pinned by digest)
+# NODE_RUNTIME_IMAGE node image used at runtime (pinned by digest)
+ARG NODE_BUILD_IMAGE=docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19
+ARG NODE_RUNTIME_IMAGE=docker.io/node:24-trixie-slim@sha256:54f84b4cf8432b21aaffd5d3ce8d41e12cea0d10d33c09d2f3c9879d49d6b446
# ---------------------------------------------------------------------------
# Build stage: clone DSH at the pinned ref, build, and package the CLI.
@@ -21,6 +22,7 @@ FROM $NODE_BUILD_IMAGE AS build
ARG DSH_REPO=https://github.com/deepseek-ai/deepseek-harness.git
ARG DSH_VERSION=dsh-v0.2.1-rc.5
+ARG DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1
ARG PNPM_VERSION=11.28.5
ENV CI=true \
@@ -31,7 +33,15 @@ RUN corepack enable \
WORKDIR /src
-RUN git clone --depth 1 --branch "$DSH_VERSION" "$DSH_REPO" .
+# Clone the pinned ref and fail closed unless it resolves to DSH_COMMIT. A moved
+# tag would otherwise silently change what the image packages.
+RUN git clone --depth 1 --branch "$DSH_VERSION" "$DSH_REPO" . \
+ && actual="$(git -C /src rev-parse HEAD)" \
+ && if [ "$actual" != "$DSH_COMMIT" ]; then \
+ echo "error: $DSH_REPO $DSH_VERSION resolved to $actual, expected DSH_COMMIT $DSH_COMMIT" >&2; \
+ echo "error: update DSH_VERSION/DSH_COMMIT in versions.env with scripts/set-version.sh" >&2; \
+ exit 1; \
+ fi
RUN pnpm install --frozen-lockfile
diff --git a/Makefile b/Makefile
index 50b3c11..11d2815 100644
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,7 @@ help: ## Show available targets
image: ## Build the main DSH Web image (no push)
docker build \
--build-arg DSH_VERSION=$(DSH_VERSION) \
+ --build-arg DSH_COMMIT=$(DSH_COMMIT) \
--build-arg RELEASE_REVISION=$(RELEASE_REVISION) \
--build-arg PNPM_VERSION=$(PNPM_VERSION) \
--build-arg VCS_REF=$(VCS_REF) \
diff --git a/SECURITY.md b/SECURITY.md
index 8ed1e37..0c4efd8 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -22,22 +22,40 @@ disclosure once a fix is available.
tenant. Keep the gateway's RBAC namespace-scoped and treat pod logs as
sensitive. Replacing log scraping with an upstream token API is tracked in
the [example architecture document](examples/dsh-agents-as-a-service/docs/architecture.md#upstream-feature-request-remove-log-scraping).
+- **Browser sessions are server-side.** The gateway session cookie holds an
+ opaque session id, not a user name or token; it is not accepted as a bearer
+ token. Sessions are revoked on logout, user delete, `/recreate` and expiry,
+ but they live in gateway memory, so a gateway restart logs every browser out
+ and scaling beyond one replica requires a shared store. Do not treat the
+ cookie as durable or cross-replica state.
- **SSO header trust boundary.** AUTH_PROXY_HEADER makes the gateway trust an
identity header (for example X-Forwarded-User). Enable it only when the
- gateway Service is reachable exclusively through the authenticating proxy;
- otherwise any client can spoof the header. The SSO exposure variant ships a
- NetworkPolicy that restricts gateway access to oauth2-proxy pods; a custom
- exposure needs an equivalent.
+ gateway Service is reachable exclusively through the authenticating proxy
+ and its NetworkPolicy; otherwise any client can spoof the header. The SSO
+ exposure variant's policy is required, not optional: an exposure without it
+ is demo-only. Run `examples/dsh-agents-as-a-service/scripts/preflight-sso.sh`
+ after applying the variant; it checks the Secrets, gateway env, TLS
+ reference, leftover placeholders and accidental second paths.
+- **Insecure-cookie guard.** TLS terminates outside the gateway. Set
+ COOKIE_SECURE=true for HTTPS and remove ALLOW_INSECURE_COOKIES; the gateway
+ refuses to start with COOKIE_SECURE=false unless ALLOW_INSECURE_COOKIES=1 is
+ explicitly set, which exists only for the plain-HTTP local demo. Never expose
+ that combination outside localhost.
+- **Abuse controls are in-process.** AUTH_RATE_LIMIT/AUTH_RATE_WINDOW,
+ SIGNUP_INVITE_CODE, MAX_IN_FLIGHT_PER_USER and WS_MAX_MESSAGE_BYTES bound
+ casual abuse and resource exhaustion on the single gateway replica. They
+ reset on restart and are not an ingress rate limiter, WAF, quota system or
+ audit log; add those for an internet-facing deployment.
- **Shared provider keys.** The example injects DEEPSEEK_API_KEY and any keys in
the optional dsh-provider-keys-extra Secret into every tenant sandbox (a
warm-pool constraint). Treat tenants as mutually trusting until per-tenant
credentials or an egress proxy exist.
-- **Optional isolation is opt-in.** gVisor, nodeSelector, the sandbox/exposure
- NetworkPolicies and extra provider keys are opt-in. Without a
- NetworkPolicy-capable CNI or a gVisor runtime, the base example is not
- tenant-isolated; see examples/dsh-agents-as-a-service/docs/optional-hardening.md.
-- **TLS is external.** Terminate TLS at an Ingress or Gateway and set
- COOKIE_SECURE=true. Plain HTTP outside localhost exposes session cookies.
+- **The base example is demo-only, not hardened.** gVisor, nodeSelector, the
+ sandbox/exposure NetworkPolicies, the gateway egress policy and extra provider
+ keys are opt-in. Without gVisor and the network policies there is no
+ tenant-to-tenant isolation; the sandboxes share the host kernel and can reach
+ cluster-internal endpoints. Production requirements are enumerated in the
+ [production checklist](examples/dsh-agents-as-a-service/docs/optional-hardening.md#6-production-checklist).
- **No secrets in the repository.** The repository only ships empty Secret
manifests and *.yaml.example files.
diff --git a/docs/main-image.md b/docs/main-image.md
index 58d3e52..8e21636 100644
--- a/docs/main-image.md
+++ b/docs/main-image.md
@@ -11,12 +11,16 @@ The image is image-first: it is the product, and the examples under
## What the build does
-The Dockerfile is a two-stage build:
+The Dockerfile is a two-stage build. Both stages use the `node` images pinned by
+digest in [versions.env](../versions.env): a mutable tag alone is not trusted, and
+`./scripts/check-versions.sh` rejects an image reference without a `@sha256:` digest.
-1. **Build stage** (`node:24-trixie`) clones DSH at the pinned git ref, installs
+1. **Build stage** (`NODE_BUILD_IMAGE`) clones DSH at the pinned git ref, verifies
+ that the clone's `HEAD` is exactly `DSH_COMMIT` (the build fails otherwise, so a
+ moved upstream tag cannot silently change what the image packages), installs
dependencies with pnpm, runs the production build, packages the `@deepseek-ai/dsh`
CLI with `pnpm pack`, and installs its production dependencies with npm.
-2. **Runtime stage** (`node:24-trixie-slim`) copies `/opt/dsh` to `/app`, adds the
+2. **Runtime stage** (`NODE_RUNTIME_IMAGE`) copies `/opt/dsh` to `/app`, adds the
CLI tools DSH agents commonly use (`git`, `ripgrep`, `jq`, `yq`, `curl`, `wget`,
`gh`, `tini`, `uv`), installs `bubblewrap` as DSH's preferred command-sandbox
runner, and starts `dsh web` through a small entrypoint.
@@ -36,6 +40,7 @@ To reproduce the equivalent by hand:
```bash
docker build \
--build-arg DSH_VERSION=dsh-v0.2.1-rc.5 \
+ --build-arg DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1 \
--build-arg RELEASE_REVISION= \
--build-arg PNPM_VERSION=11.28.5 \
--build-arg VCS_REF="$(git rev-parse --short HEAD)" \
@@ -48,11 +53,17 @@ docker build \
| --- | --- | --- |
| `DSH_REPO` | `https://github.com/deepseek-ai/deepseek-harness.git` | Upstream DSH repository to clone. |
| `DSH_VERSION` | `DSH_VERSION` in [versions.env](../versions.env) | Upstream DSH git tag/ref to package. The Dockerfile records it as `io.sredevopsorg.dsh.version` and (for local builds) `org.opencontainers.image.version`; published builds set the version label to the full release identity. |
+| `DSH_COMMIT` | `DSH_COMMIT` in [versions.env](../versions.env) | Full commit SHA `DSH_VERSION` must resolve to upstream. The build stage fails unless `git rev-parse HEAD` in the clone equals this value, which pins the source independently of a movable tag. |
| `RELEASE_REVISION` | `RELEASE_REVISION` in [versions.env](../versions.env) | Repository revision suffix (`rN`), recorded in `io.sredevopsorg.release.revision`. The release identity is `DSH_VERSION` plus `-RELEASE_REVISION` when set. See [versioning](versioning.md). |
| `PNPM_VERSION` | `PNPM_VERSION` in [versions.env](../versions.env) | pnpm version activated with corepack in both stages. |
| `VCS_REF` | `git rev-parse --short HEAD` (via `make image`) | Source revision recorded for traceability. |
-| `NODE_BUILD_IMAGE` | `docker.io/node:24-trixie` | Build-stage base image. |
-| `NODE_RUNTIME_IMAGE` | `docker.io/node:24-trixie-slim` | Runtime-stage base image. |
+| `NODE_BUILD_IMAGE` | `docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19` | Build-stage base image, pinned to the multi-arch index digest. |
+| `NODE_RUNTIME_IMAGE` | `docker.io/node:24-trixie-slim@sha256:54f84b4cf8432b21aaffd5d3ce8d41e12cea0d10d33c09d2f3c9879d49d6b446` | Runtime-stage base image, pinned to the multi-arch index digest. |
+
+Base images are pinned by digest because tags are mutable: `node:24-trixie` can be
+re-pointed at a new build at any time. Bumping the tag and its digest is a
+deliberate, reviewable change in [versions.env](../versions.env); environments that
+mirror the image should also mirror the `node` digest they build from.
Never hard-code a version outside [versions.env](../versions.env):
`./scripts/check-versions.sh` (and `make check`) fails when a pin drifts.
diff --git a/docs/versioning.md b/docs/versioning.md
index 68d6ea3..66b6a17 100644
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -61,14 +61,27 @@ Values from [versions.env](../versions.env) at the current identity:
| --- | --- | --- |
| Upstream DSH source | `DSH_REPO` | [versions.env](../versions.env) |
| DSH ref | `dsh-v0.2.1-rc.5` | `DSH_VERSION` |
+| DSH commit | `b830ec950faba0a46dda564be3e2d854afed94f1` | `DSH_COMMIT`; the main image build fails unless the clone's `HEAD` matches |
| Repository revision | `(empty)` | `RELEASE_REVISION` |
| pnpm | `11.28.5` | `PNPM_VERSION` |
+| Node build image | `docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19` | `NODE_BUILD_IMAGE`, digest-pinned |
+| Node runtime image | `docker.io/node:24-trixie-slim@sha256:54f84b4cf8432b21aaffd5d3ce8d41e12cea0d10d33c09d2f3c9879d49d6b446` | `NODE_RUNTIME_IMAGE`, digest-pinned |
+| Gateway Python image | `docker.io/python:3.12-slim@sha256:aa943ce411423c7582288f18707ff66eddcbaeb510502d747a72aeb34f30d159` | `GATEWAY_PYTHON_IMAGE`, digest-pinned |
| agent-sandbox (CRDs) | `v1.0.9` | `AGENT_SANDBOX_VERSION`, used by the examples and CI dry-run |
| Kubernetes Gateway API | `v1.2.1` | `GATEWAY_API_VERSION`, used by the SSO exposure variant |
-| oauth2-proxy | `quay.io/oauth2-proxy/oauth2-proxy:v7.15.5` | `OAUTH2_PROXY_IMAGE` |
+| oauth2-proxy | `quay.io/oauth2-proxy/oauth2-proxy:v7.15.5@sha256:8498b0d0ef0a7b29686414000a08aee467f02d0299c9ed1e006a8f33fc017916` | `OAUTH2_PROXY_IMAGE`, digest-pinned and used by the SSO exposure variant |
| Main image | `ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox` | `GHCR_IMAGE` |
| Gateway image | `ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway` | `GATEWAY_IMAGE`, nested below `GHCR_IMAGE` |
+Every container base image is referenced as `name:tag@sha256:`, not
+by tag alone: `check-versions.sh` rejects a pin without a digest, so a re-pushed
+tag cannot change what we build from. The example gateway's Python dependencies
+are fully hash-pinned in
+[requirements.txt](../examples/dsh-agents-as-a-service/gateway/requirements.txt)
+and
+[requirements-dev.txt](../examples/dsh-agents-as-a-service/gateway/requirements-dev.txt)
+and installed with `pip install --require-hashes`.
+
Example component images are always nested below the main package:
`//`. `check-versions.sh` rejects an unrelated
top-level package.
@@ -86,8 +99,15 @@ file to derive from it. Never edit the Dockerfile, manifests, Makefile or
workflows with a literal version.
- New upstream DSH ref: change `DSH_VERSION` and clear `RELEASE_REVISION`.
-- Repository-only change at the same DSH ref: keep `DSH_VERSION`, set
- `RELEASE_REVISION=r(N+1)`.
+ `set-version` resolves the new ref's commit with `git ls-remote` (peeled `^{}`
+ commit first, tag ref as fallback) and rewrites `DSH_COMMIT` in `versions.env`,
+ the Dockerfile ARG default and `.env.sample`; the build then verifies it.
+- Repository-only change at the same DSH ref: keep `DSH_VERSION` and `DSH_COMMIT`,
+ set `RELEASE_REVISION=r(N+1)`.
+- Base images (Node and Python) are bumped by hand: change the tag *and* the
+ `@sha256:` digest in `versions.env` together, then run `make check` and a build.
+ There is deliberately no automated digest refresh, because a silent base-image
+ change is exactly what digest pinning exists to prevent.
## Release checklist
@@ -98,7 +118,8 @@ workflows with a literal version.
the tag does not already exist, and previews the generated notes.
5. Create and push the annotated `dsh-*` release tag (or dispatch
`release.yaml` with an existing tag). The workflow validates that the tag
- matches [versions.env](../versions.env).
+ matches [versions.env](../versions.env) and that the upstream ref still
+ resolves to `DSH_COMMIT` before building anything.
6. The workflow builds and pushes multi-arch images for the main image and the
example components (the identity tag plus `latest`), creates the GitHub
Release from the git-cliff notes, and opens a `chore(changelog)` PR that
@@ -142,8 +163,17 @@ make changelog # regenerate CHANGELOG.md locally
## Digest pinning
-Release identities are mutable only in theory; deployments should still pin the
-resolved digest for reproducibility and supply-chain safety:
+Supply-chain inputs are pinned to immutable content, not to mutable tags:
+
+- **Build inputs**: every container base image is referenced by multi-arch index
+ digest in [versions.env](../versions.env), and the example gateway's Python
+ dependencies are hash-pinned (`--require-hashes`).
+- **Upstream source**: the Dockerfile clones `DSH_VERSION` and fails unless the
+ clone's `HEAD` equals `DSH_COMMIT`, so a moved upstream tag cannot change what
+ a rebuild packages.
+
+Release identities are immutable in theory; deployments should still pin the
+_resolved_ digest for reproducibility and supply-chain safety:
```bash
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
diff --git a/examples/dsh-agents-as-a-service/README.md b/examples/dsh-agents-as-a-service/README.md
index 2f3b29d..c6105a4 100644
--- a/examples/dsh-agents-as-a-service/README.md
+++ b/examples/dsh-agents-as-a-service/README.md
@@ -16,8 +16,9 @@ It ships in three layers:
1. **The resource layer** (steps 1–7): pure manifests + kubectl, so every
mechanism is visible.
2. **The service layer** (step 8): a small included **gateway** (`gateway/`) that
- makes the platform browser-usable — a signup page/API, `HttpOnly` cookie
- sessions, a `/u/{user}/**` proxy for HTTP **and WebSocket** that transparently
+ makes the platform browser-usable — a signup page/API, revocable server-side
+ **opaque-cookie sessions**, auth rate limiting and an optional signup invite
+ code, a `/u/{user}/**` proxy for HTTP **and WebSocket** that transparently
**wakes suspended instances on connect**, and an idle sweeper that suspends
them again. The gateway performs the DSH launch-token handshake server-side,
so the browser never sees the token.
@@ -65,12 +66,13 @@ What it demonstrates, feature by feature:
upstream instead of splitting `/v1/*` (API) from the dashboard.
- **Shared provider keys**: `DEEPSEEK_API_KEY` plus any keys in the optional
`dsh-provider-keys-extra` Secret are injected via `envFrom` into every
- sandbox. There is no per-tenant key, dashboard basic-auth, session-cookie
- secret, or API-server key.
+ sandbox. There is no per-tenant key, dashboard basic-auth, signed-session
+ secret (sessions are opaque server-side ids), or API-server key.
- **Launch-token handshake**: DSH gates `/` behind a token it prints only in its
logs. The gateway reads it, performs the handshake, and keeps the resulting
auth cookie server-side; the browser authenticates to the gateway with its own
- cookie. See [How the DSH handshake works](#how-the-dsh-handshake-works).
+ opaque session cookie, never the token. See
+ [How the DSH handshake works](#how-the-dsh-handshake-works).
- **Host allow-list**: DSH rejects unknown `Host` headers. The gateway forwards
the browser's `Host`, so the public hostname must be added to
`DSH_TRUSTED_HOSTS` in the template.
@@ -285,6 +287,15 @@ kubectl -n dsh-demo port-forward svc/dsh-gateway 18080:8080
> The gateway binds `8080`; the local port-forward uses `18080` and never `3080`.
>
+> **Cookie/TLS guard.** The base manifest serves plain HTTP through port-forward,
+> so it sets `COOKIE_SECURE=false` **and** `ALLOW_INSECURE_COOKIES=1`. The gateway
+> refuses to start when `COOKIE_SECURE=false` unless
+> `ALLOW_INSECURE_COOKIES=1` explicitly acknowledges that the session cookie
+> travels in clear text; that prevents an HTTPS deployment from silently serving
+> an insecure cookie. Before exposing the gateway over TLS, set
+> `COOKIE_SECURE=true` and remove the acknowledgement:
+> `kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-`.
+>
> **Image pull policy.** `manifests/50-gateway.yaml` uses
> `imagePullPolicy: IfNotPresent` with the release identity, so a locally built
> image must be loaded into the cluster under exactly that name (kind:
@@ -317,25 +328,40 @@ kubectl -n dsh-demo port-forward svc/dsh-gateway 18080:8080
### Browser flow
-Open . The landing page has a one-field signup form.
-Enter `bob`, and the gateway:
+Open . The landing page has a one-field signup form (plus
+an invite-code field when `SIGNUP_INVITE_CODE` is configured). Enter `bob`, and
+the gateway:
-1. creates the `SandboxClaim` over the warm pool,
-2. waits for it to become `Ready`,
-3. sets an `HttpOnly`, `SameSite=Lax` cookie `dsh_gw_session=:` on
- the browser,
-4. redirects to `/u/bob/`, where the DSH Web UI loads through the gateway.
+1. enforces the auth rate limit and, if configured, the signup invite code,
+2. creates the `SandboxClaim` over the warm pool,
+3. waits for it to become `Ready`,
+4. mints a random, server-side session (the cookie value is an opaque `sid`, not
+ a user name and not a token), stores `sid -> (user, expiry)` in memory, and
+ sets the `HttpOnly`, `SameSite=Lax` cookie `dsh_gw_session=` on the
+ browser (`__Host-dsh_gw_session` when `COOKIE_SECURE=true`),
+5. redirects to `/u/bob/`, where the DSH Web UI loads through the gateway.
From then on every request under `/u/bob/` (assets, API calls, SSE, WebSocket) is
proxied to bob's sandbox. The DSH launch token and DSH's own auth cookie stay in
-the gateway; the browser only holds the gateway cookie.
+the gateway; the browser only holds the opaque gateway session id. The cookie is
+useless as a bearer token.
+
+**Sessions are revocable and mostly short-lived in practice.** `POST /logout`,
+deleting the user, `POST /recreate` and cookie expiry (default 7 days,
+`COOKIE_MAX_AGE`) invalidate the server-side session. The session table lives in
+gateway memory, so **restarting the gateway logs every browser out** — expected
+for this single-replica demo; a production deployment needs a shared session
+store and sticky routing or per-request re-authentication. With SSO enabled, the
+gateway mints a session for the identity oauth2-proxy asserts instead of asking
+for a token.
The same flow with `curl`, no JavaScript (the session cookie is the only
credential):
```bash
JAR=$(mktemp)
-# signup (form POST, exactly what the landing page sends)
+# signup (form POST, exactly what the landing page sends); add -d 'invite=...'
+# when SIGNUP_INVITE_CODE is configured, or send X-Signup-Invite
curl -s -i -c "$JAR" -H 'Origin: http://localhost:18080' \
-d 'user=bob' http://localhost:18080/users
# proxy: bob's DSH Web through the gateway
@@ -344,13 +370,38 @@ curl -s -b "$JAR" http://localhost:18080/u/bob/ | head -5
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/u/bob/ # 401
# derived state: Ready / Waking / Suspended / ...
curl -s -b "$JAR" http://localhost:18080/users/bob
-# delete bob (cascades sandbox + PVC)
+# sign out (revokes the server-side session)
+curl -s -b "$JAR" -H 'Origin: http://localhost:18080' -X POST http://localhost:18080/logout
+# delete bob (cascades sandbox + PVC and revokes every session for bob)
curl -s -b "$JAR" -H 'Origin: http://localhost:18080' -X DELETE http://localhost:18080/users/bob
```
API clients that prefer bearer tokens can use `POST /users` with a JSON body
(`{"user":"bob"}`) and the returned one-time token, `POST /login` to exchange a
-token for a cookie, and `Authorization: Bearer ` on subsequent calls.
+token for a cookie, and `Authorization: Bearer ` on subsequent calls. A
+cookie/session value is never accepted as a bearer token, and a token for one
+user cannot read another user's path.
+
+### Auth abuse controls and proxy limits
+
+The gateway is a single process, so these limits are per replica. They stop
+casual abuse and one tenant monopolizing the service; an internet-facing
+deployment still needs ingress-level rate limiting and an audit trail.
+
+| Setting | Default | Behavior |
+| --- | --- | --- |
+| `AUTH_RATE_LIMIT` / `AUTH_RATE_WINDOW` | `30` per `60` s | Throttles `/users` and `/login` per client (rightmost `X-Forwarded-For` when behind a proxy, else the TCP peer) and answers `429` + `Retry-After` before reading the body or touching the cluster. `0` disables it. The base manifest sets it explicitly. |
+| `SIGNUP_INVITE_CODE` | unset | When set, `POST /users` requires the code as form field `invite` or header `X-Signup-Invite` (constant-time compare, `403` before user lookup). Put it in a Secret and restart the gateway to rotate. |
+| `MAX_IN_FLIGHT_PER_USER` | `32` | Caps concurrent proxied requests + WebSockets per user; excess HTTP gets `503` + `Retry-After`, excess WebSocket upgrades close with `1011`. |
+| `WS_MAX_MESSAGE_BYTES` | `16 MiB` | Largest WebSocket message received from DSH; oversized frames close the connection. Browser-to-DSH frames are bounded by uvicorn's own limit. HTTP/SSE streaming is not capped by this. |
+| `MAX_BODY_MB` | `32` | Largest proxied HTTP request body (`413`). |
+
+The gateway also strips client-supplied `Forwarded`, `X-Forwarded-User` and
+`X-Auth-Request-*` headers (and `x-forwarded-*` / `x-auth-request-*` prefixes)
+before proxying, so a tenant cannot inject identity or spoof hop headers into
+DSH. `UPSTREAM_HEADER_ALLOWLIST` can replace that denylist with an explicit
+comma-separated allowlist. See the [gateway README](gateway/README.md) for the
+full configuration.
### Gateway routes
@@ -395,8 +446,11 @@ not cut off.
The upstream upgrade dials the sandbox **pod IP**, forwards no `Host`/`Origin`,
and performs the launch-token handshake under the same `pod-IP:3080` authority.
Missing or cross-origin auth closes with `4401`; upstream bridging failures close
-with `1011`. See the [gateway README](gateway/README.md#proxy-failure-semantics)
-for the full matrix.
+with `1011`, as does exceeding `MAX_IN_FLIGHT_PER_USER` (per-user socket cap).
+Messages received from DSH larger than `WS_MAX_MESSAGE_BYTES` (16 MiB by
+default) close the connection instead of being buffered; browser-to-DSH frames
+are bounded by uvicorn's own limit. See the
+[gateway README](gateway/README.md#proxy-failure-semantics) for the full matrix.
### Path routing
@@ -420,17 +474,20 @@ not a stack. Details, commands and the switch procedure are in the
Both variants also ship NetworkPolicies: `ingress/61-networkpolicy.yaml` limits
gateway ingress to Traefik pods, and `gateway-api-sso/75-networkpolicies.yaml`
limits the gateway to oauth2-proxy and oauth2-proxy egress to DNS, public HTTPS
-and the gateway. They are part of the variant; see the
-[exposure README](manifests/exposure/README.md) and
-[optional hardening](docs/optional-hardening.md).
+and the gateway. They are part of the variant **and required for production**:
+an exposure applied without its policy is demo-only. The sandbox side needs a
+separate policy, and the gateway needs an egress policy; both are covered in
+[optional hardening](docs/optional-hardening.md) (see the production checklist)
+and summarized in the [exposure README](manifests/exposure/README.md).
### Option A — plain Ingress (gateway's own session auth)
1. Set the Ingress host/TLS (or drop the `tls:` block), and set
`DSH_TRUSTED_HOSTS` in `manifests/10-sandboxtemplate.yaml` to the same public
host; re-apply the template.
-2. Set `COOKIE_SECURE=true` once the Ingress serves HTTPS:
- `kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true`.
+2. Set `COOKIE_SECURE=true` and drop the plain-HTTP acknowledgement once the
+ Ingress serves HTTPS:
+ `kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-`.
3. `kubectl apply -f manifests/exposure/ingress/`; verify
`curl -sI https:///healthz` returns `200`.
@@ -453,21 +510,37 @@ front of **oauth2-proxy**, which authenticates the browser against GitHub
2. Create a GitHub OAuth App whose homepage/callback match the public FQDN, and
create the oauth2-proxy and TLS Secrets from
`73-oauth2-proxy-secret.yaml.example` and `74-tls-secret.yaml.example`.
-3. Replace `dsh-demo.example.com` and `your-github-org`, set
- `DSH_TRUSTED_HOSTS` in the template, and enable SSO:
- `kubectl -n dsh-demo set env deploy/dsh-gateway AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true`.
-4. `kubectl apply -f manifests/exposure/gateway-api-sso/`; verify the Gateway,
- HTTPRoute, oauth2-proxy Deployment and endpoints as shown in the
+3. Replace `dsh-demo.example.com` and `your-github-org` in the variant
+ manifests, set `DSH_TRUSTED_HOSTS` in the template, and enable SSO:
+ `kubectl -n dsh-demo set env deploy/dsh-gateway AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-`.
+4. `kubectl apply -f manifests/exposure/gateway-api-sso/`; then run
+ [`scripts/preflight-sso.sh`](scripts/preflight-sso.sh) and fix everything it
+ reports **before** pointing real traffic at the Gateway. It verifies the
+ Secrets (including a valid oauth2-proxy cookie secret length), the gateway's
+ `AUTH_PROXY_HEADER`/`COOKIE_SECURE`, that the Gateway's TLS certificate
+ reference resolves, and that no `your-github-org`/`dsh-demo.example.com`
+ placeholder was left applied. Full verification steps are in the
[exposure README](manifests/exposure/README.md).
+> **Exposure without the variant NetworkPolicy is demo-only.** The SSO
+> variant's `75-networkpolicies.yaml` is not optional in practice: it is the
+> only thing that makes oauth2-proxy the sole path to the gateway. Applying
+> only the Gateway/HTTPRoute/oauth2-proxy objects leaves the trust boundary
+> unenforced, so anyone who can reach `dsh-gateway:8080` can send
+> `X-Forwarded-User` and impersonate the user they claim to be.
+>
> **SSO trust boundary — read this before enabling it.** After SSO is on, the
> `dsh-gateway` Service must be reachable **only** through oauth2-proxy (it is
> ClusterIP). The variant's `75-networkpolicies.yaml` restricts pod traffic
> accordingly: the gateway accepts traffic only from oauth2-proxy pods,
> oauth2-proxy only from the Gateway implementation, and its egress only to DNS,
-> public HTTPS and the gateway. Anyone who can reach `dsh-gateway:8080` directly
-> can send `X-Forwarded-User: `, so never add a second path. To turn SSO
-> off, remove the variable
+> public HTTPS and the gateway. The gateway's own egress (Kubernetes API,
+> sandbox pods) is not restricted by that file because the API-server endpoint is
+> cluster-specific; add the
+> [gateway egress policy](docs/optional-hardening.md#gateway-egress-required-for-production)
+> for production. Anyone who can reach `dsh-gateway:8080` directly can send
+> `X-Forwarded-User: `, so never add a second path. To turn SSO off,
+> remove the variable
> (`kubectl -n dsh-demo set env deploy/dsh-gateway AUTH_PROXY_HEADER-`) and
> delete the SSO variant.
@@ -481,25 +554,36 @@ gateway remains. Full commands and verification are in the
## Optional hardening
-The base walkthrough is deliberately portable. Five opt-in settings are
-documented in [optional hardening](docs/optional-hardening.md):
+The base walkthrough is deliberately portable, which means it is **not
+hardened**: without the policies and gVisor it is demo-only, not a safe
+multi-tenant platform. [Optional hardening](docs/optional-hardening.md) has the
+settings and a production checklist. In short:
- **gVisor** — uncomment `runtimeClassName: gvisor` before applying, or patch an
existing install. Requires a `gvisor` RuntimeClass and capable nodes.
`kubectl port-forward` does not work under gVisor; the gateway path does.
+ Required for untrusted agents in production.
- **nodeSelector** — uncomment and replace the placeholder node label to pin
sandboxes and the gateway to a dedicated pool; add a toleration for tainted
pools.
- **Sandbox NetworkPolicy** —
`kubectl apply -f manifests/optional/network-policy/` allows only
- gateway→sandbox on `3080` and sandbox→DNS/public `443`.
+ gateway→sandbox on `3080` and sandbox→DNS/public `443`. Required for
+ production; exposure without it is demo-only.
+- **Gateway egress policy** — the gateway needs DNS, the Kubernetes API and
+ `3080` to sandbox pods. It is not shipped because the API-server endpoint is
+ cluster-specific; use the
+ [recipe](docs/optional-hardening.md#gateway-egress-required-for-production).
+ Required for production.
- **Provider keys** — create `dsh-provider-keys-extra` as shown in step 1.
- **Failed-Pod recovery** — uncomment `failedPodPolicy: Recreate` so the
controller replaces a `Failed` sandbox Pod (node eviction) instead of parking
the sandbox at `Finished=True`. Requires agent-sandbox v1.0.8+.
-Apply each independently and re-run the walkthrough. `kubectl apply -f manifests/`
-is non-recursive, so the optional manifests are not applied by the base step.
+The **exposure variants' NetworkPolicies** are not optional in production
+either; see step 9. Apply each setting independently and re-run the walkthrough.
+`kubectl apply -f manifests/` is non-recursive, so the optional manifests are not
+applied by the base step.
## Troubleshooting
@@ -508,9 +592,16 @@ is non-recursive, so the optional manifests are not applied by the base step.
| Pod `CrashLoopBackOff`, probes failing with `401` | Probe path is `/`; change readiness/liveness to `/favicon.svg` |
| Browser shows `401` from DSH through the gateway | Gateway handshake failed — check `kubectl -n dsh-demo logs deploy/dsh-gateway` and that RBAC grants `pods/log` |
| `502` / `upstream unavailable` | Sandbox pod is not Ready (wake timed out) or the Service DNS is stale; check `kubectl get sandbox`, `WAKE_TIMEOUT` |
-| `503` with `Retry-After` | The launch token was not found in the pod logs yet; the gateway retries after a short delay. Check the pod logs and `DSH_LOG_TAIL_LINES` |
+| `503` with `Retry-After` | Either the launch token was not found in the pod logs yet (the gateway retries after a short delay; check the pod logs and `DSH_LOG_TAIL_LINES`) or the user hit `MAX_IN_FLIGHT_PER_USER` (default 32 concurrent requests/WebSockets); reduce client concurrency or raise the limit deliberately |
+| `429` with `Retry-After` on signup/login | The per-client auth rate limit (`AUTH_RATE_LIMIT`, default explicitly `30` per `AUTH_RATE_WINDOW=60` s) rejected the attempt before reading the body. Wait for the window, or raise the limit; behind a proxy make sure the real client IP is the rightmost `X-Forwarded-For` entry, not a shared NAT address |
+| `403` on signup: invite required/invalid | `SIGNUP_INVITE_CODE` is configured. Send the code as form field `invite` or header `X-Signup-Invite`; the landing page shows the field when enabled. A wrong code is rejected before the user lookup |
+| Gateway pod `CrashLoopBackOff` with `refusing to start with an insecure session cookie` | Intentional guard: plain HTTP is only allowed when explicitly acknowledged. For the local demo set `ALLOW_INSECURE_COOKIES=1` (the base manifest does); for HTTPS set `COOKIE_SECURE=true` and remove `ALLOW_INSECURE_COOKIES` |
+| Every browser is logged out after a gateway rollout/restart | Expected: sessions are opaque ids in gateway memory and a restart drops them. Sign in again; a production deployment needs shared session state or per-request re-authentication. The DSH sandboxes and their data are unaffected |
+| WebSocket closes with `1011` for a busy client | `MAX_IN_FLIGHT_PER_USER` (per-user connection cap) or an upstream/handshake failure closed the socket; check the gateway logs to tell them apart |
+| WebSocket closes after a very large message from DSH | A message received from DSH exceeded `WS_MAX_MESSAGE_BYTES` (16 MiB), so the relay closed it instead of buffering; raise the limit only if DSH legitimately sends larger frames. Browser-to-DSH frames are bounded by uvicorn's own limit |
+| `scripts/preflight-sso.sh` exits non-zero | It names the failing item: a missing Secret/key, a bad oauth2-proxy cookie-secret length, missing `AUTH_PROXY_HEADER`/`COOKIE_SECURE`, a dangling Gateway TLS reference, a leftover `your-github-org`/`dsh-demo.example.com` placeholder, or a second path to the gateway (an Ingress). Fix all items before exposing the SSO variant |
| `409` on signup | User already has a claim; use `POST /login` with the existing token or delete the user first |
-| Cookie set but browser keeps getting `401` | `COOKIE_SECURE=true` while serving over plain HTTP (secure cookies are not sent); use HTTPS or set `false` for local demos |
+| Cookie set but browser keeps getting `401` | `COOKIE_SECURE=true` while serving over plain HTTP (secure cookies are not sent); use HTTPS or set `false` **and** `ALLOW_INSECURE_COOKIES=1` for local demos |
| WebSocket closes immediately (`4401`) | Missing/expired gateway cookie, or the upgrade's `Origin` does not match `Host` (CSWSH check); DSH rejecting `Host` instead surfaces as close `1011` — add the host to `DSH_TRUSTED_HOSTS` |
| Console spams `[connection] connection lost, retry #N` | The upstream WS upgrade is being rejected, surfaced as close `1011`. The gateway dials the sandbox **pod IP**, forwards no `Host`/`Origin`, and handshakes under the same `pod-IP:3080` authority DSH binds its cookie to; an older gateway image, or a sandbox with an empty `status.podIPs`, hits this loop |
| `manifest.webmanifest` returns `401` in the console | Browsers fetch the Web App Manifest without credentials, so the gateway's session cookie is absent and the request fails closed. The UI is unaffected; only the PWA install prompt is lost |
@@ -539,9 +630,19 @@ is non-recursive, so the optional manifests are not applied by the base step.
Deliberately **not** production code:
-- **Single replica.** Idle clocks, in-flight tracking, and the handshake cache
- are in memory. Running two gateway replicas would double-suspend or lose wake
- tracking; scale up only after moving that state to a shared store.
+- **Single replica.** Idle clocks, in-flight tracking, the handshake cache and
+ the browser session table are in memory. Running two gateway replicas would
+ double-suspend or lose wake tracking, and sessions would land on the wrong
+ replica; scale up only after moving that state to a shared store.
+- **Sessions die with the process.** Browser sessions are opaque ids held in
+ gateway memory (revocable on logout/delete/recreate, but not durable). A
+ gateway restart logs every browser out. There is no SSO refresh-token or
+ password store; identity is re-established by oauth2-proxy on the next
+ request.
+- **Abuse controls are per-process and coarse.** `AUTH_RATE_LIMIT`, the signup
+ invite code, `MAX_IN_FLIGHT_PER_USER` and `WS_MAX_MESSAGE_BYTES` limit casual
+ abuse, but they are not a substitute for ingress rate limiting, WAF/quota, or
+ an audit log, and the in-memory rate buckets reset on restart.
- **Log scraping.** The launch token is recovered from pod logs. Log rotation, a
restarted pod, or a changed DSH log format can break the handshake until the
next `401` refresh.
@@ -551,12 +652,13 @@ Deliberately **not** production code:
but the platform does not manage that. Real multi-tenancy needs per-tenant
credentials or an egress proxy.
- **No TLS in the gateway.** Terminate TLS at an Ingress or Gateway and set
- `COOKIE_SECURE=true`.
+ `COOKIE_SECURE=true`, then remove `ALLOW_INSECURE_COOKIES`. The base manifest
+ runs plain HTTP for the port-forward demo and is demo-only.
- **SSO trust is hop-based.** `AUTH_PROXY_HEADER` trusts whoever sets the header.
The SSO variant ships `75-networkpolicies.yaml`, which restricts the gateway
to oauth2-proxy and oauth2-proxy egress to the gateway/DNS/public HTTPS; a
custom exposure without that policy has no signature, mTLS or network
- enforcement.
+ enforcement. Exposure without the policy is demo-only.
- **DSH's inner file sandbox is off here.** The template sets
`DSH_PERMISSION_MODE=danger-full-access` because neither backend of DSH's own
command sandbox is usable in this pod: `bubblewrap` is installed in the image
@@ -567,21 +669,24 @@ Deliberately **not** production code:
pod remains the boundary — non-root,
capabilities dropped, `seccompProfile: RuntimeDefault`, its own PVC and Service
— but an agent command can now read its own `DSH_HOME` (including
- `.credentials.yaml`) and write anywhere in the container. On hosts where a
- backend works, set `workspace-write` (or `read-only`) and verify before relying
- on the inner policy.
-- **Isolation is opt-in.** gVisor, the sandbox NetworkPolicy and the exposure
- policies are opt-in; without them, sandboxes share the host kernel and can
- reach the cluster network. The base example is a demo, not a hardened
- multi-tenant platform.
+ `.credentials.yaml`) and write anywhere in the container. Run sandboxes under
+ gVisor in production. On hosts where a backend works, set `workspace-write`
+ (or `read-only`) and verify before relying on the inner policy.
+- **Isolation is opt-in.** gVisor, the sandbox NetworkPolicy, the gateway egress
+ policy and the exposure policies are opt-in; without them, sandboxes share the
+ host kernel and can reach the cluster network. The base example is a demo, not
+ a hardened multi-tenant platform. See the
+ [production checklist](docs/optional-hardening.md#6-production-checklist).
- **OAuth org is the only authorization.** Any GitHub user in the configured
organization gets an auto-provisioned sandbox; there is no per-user allow-list,
- quota, or admin API.
+ quota, or admin API. `SIGNUP_INVITE_CODE` gates the gateway's own signup form,
+ not SSO auto-provisioning.
- **In-memory user directory.** Users are claims + annotations; there is no
database, password reset, or admin API. Tokens are stored only as SHA-256
hashes in claim annotations.
- **Bearer tokens in URLs.** Avoid putting the signup token in a URL/log; the
- browser flow uses the `HttpOnly` cookie instead.
+ browser flow uses the `HttpOnly` cookie instead. The cookie no longer contains
+ the token, and a cookie value is never accepted as a bearer token.
## Cleanup
@@ -603,6 +708,8 @@ delete the Secrets with the namespace and revoke the OAuth App in GitHub.
- [Architecture](docs/architecture.md) — container/sequence diagrams, ADRs,
security and RBAC, failure modes, evolution path.
- [Exposure README](manifests/exposure/README.md) — the two variants in detail.
+- [SSO preflight](scripts/preflight-sso.sh) — read-only checks for the SSO
+ variant (Secrets, gateway env, TLS reference, placeholders, second paths).
- [Optional hardening](docs/optional-hardening.md) — gVisor, nodeSelector,
- NetworkPolicy, extra provider keys and failed-Pod recovery, with verification
- and rollback.
+ NetworkPolicy, extra provider keys and failed-Pod recovery, with verification,
+ the production checklist and rollback.
diff --git a/examples/dsh-agents-as-a-service/docs/architecture.md b/examples/dsh-agents-as-a-service/docs/architecture.md
index 8028c4c..a9fe76e 100644
--- a/examples/dsh-agents-as-a-service/docs/architecture.md
+++ b/examples/dsh-agents-as-a-service/docs/architecture.md
@@ -72,8 +72,10 @@ Each variant also ships a NetworkPolicy: `ingress/61-networkpolicy.yaml` limits
to Traefik pods, and `gateway-api-sso/75-networkpolicies.yaml` limits the gateway to
oauth2-proxy, oauth2-proxy ingress to the Gateway implementation, and oauth2-proxy egress to
DNS/public HTTPS/the gateway. The SSO policy turns the trust boundary into pod-traffic
-enforcement; the caveats (Traefik label, `hostNetwork`, kubelet probes) are in
-[optional hardening](optional-hardening.md).
+enforcement. These policies are **required for production**: exposing the gateway by applying
+only the Gateway/HTTPRoute/Ingress objects is demo-only. The sandbox-side policy and the
+gateway's own egress policy are separate (the API-server endpoint is cluster-specific); see
+[optional hardening](optional-hardening.md#gateway-egress-required-for-production).
## Flows
@@ -87,20 +89,22 @@ sequenceDiagram
participant K as Kubernetes API
participant C as agent-sandbox controller
- B->>G: POST /users (form: user=bob, Origin)
- G->>G: validate user name and body size
+ B->>G: POST /users (form: user=bob, invite, Origin)
+ G->>G: rate limit, invite check, validate user name and body size
G->>K: create SandboxClaim dsh-bob (warmPoolRef)
K->>C: reconcile claim
C-->>K: adopt spare, Ready, status.sandbox.name = dsh-pool-xxxx
G->>K: read claim (status) + sandbox + pod
- G-->>B: 303 Location /u/bob/ + Set-Cookie dsh_gw_session
+ G->>G: mint opaque sid, store sid -> (user, expiry)
+ G-->>B: 303 Location /u/bob/ + Set-Cookie dsh_gw_session=
Note over G: JSON clients instead get 201 (Ready), 202 (accepted, still waking), or 409 (conflict)
```
Form signup waits up to `WAKE_TIMEOUT` for the claim to become `Ready`, then redirects; the
first proxied request also waits for `Ready` and performs the DSH handshake (below). A `409`
means the user already has a claim, in which case `POST /login` (bearer token) or the existing
-cookie is the right path.
+cookie is the right path. The cookie value is an opaque session id: it is meaningless outside
+this gateway process and is never accepted as a bearer token.
### First request / proxy (with handshake)
@@ -112,8 +116,8 @@ sequenceDiagram
participant K as Kubernetes API
participant D as DSH Web pod
- B->>G: GET /u/bob/ (Cookie dsh_gw_session)
- G->>G: authenticate cookie, user = bob
+ B->>G: GET /u/bob/ (Cookie dsh_gw_session=)
+ G->>G: look up sid -> session (valid, not expired), user = bob
G->>K: resolve claim -> sandbox -> pod
alt sandbox Suspended
G->>K: patch operatingMode=Running (wake)
@@ -140,6 +144,32 @@ Inside `proxy_http` the order is auth, in-flight slot, sandbox resolution/wake,
body read, upstream send. An upload aborted mid-body may therefore already have paid for a wake
and a handshake, but it never reaches DSH.
+### Browser sessions
+
+The browser never holds a DSH credential. On signup/login the gateway mints a random session id
+(`secrets.token_urlsafe(32)`), stores `sid -> Session(user, expires_at)` in memory, and sets it
+as the `HttpOnly`, `SameSite=Lax` cookie `dsh_gw_session` (`__Host-dsh_gw_session` when
+`COOKIE_SECURE=true`). Every browser request is resolved through that table; the cookie value is
+not a bearer token and cannot be replayed against another user's path.
+
+Sessions end on explicit logout, user delete, `/recreate` (all sessions for that user) and
+expiry (`COOKIE_MAX_AGE`, default 7 days). Because the table is process memory, a gateway
+restart logs every browser out; scaling beyond one replica therefore requires a shared session
+store (or re-authentication on every request). The sweeper prunes expired entries and sessions
+for users whose claim no longer exists.
+
+### Auth abuse controls and proxy limits
+
+Unauthenticated endpoints are throttled before the body is read: `AUTH_RATE_LIMIT` attempts per
+client per `AUTH_RATE_WINDOW` (defaults 30/60s; keyed on the rightmost `X-Forwarded-For` entry
+when present, else the peer address), answered with `429` + `Retry-After`. When
+`SIGNUP_INVITE_CODE` is set, signup additionally requires a constant-time-matched code (form
+field or `X-Signup-Invite`) and answers `403` otherwise. Proxied traffic is capped per user
+(`MAX_IN_FLIGHT_PER_USER`, default 32 concurrent HTTP requests + WebSockets: `503` for HTTP,
+close `1011` for WebSocket) and per WebSocket message received from DSH
+(`WS_MAX_MESSAGE_BYTES`, 16 MiB). These are in-process guards for a single replica, not an ingress
+rate limiter or quota system.
+
### Wake-on-connect and idle suspend
```mermaid
@@ -220,6 +250,14 @@ sequenceDiagram
end
```
+An upgrade with a missing/expired session, or one that would exceed
+`MAX_IN_FLIGHT_PER_USER`, is accepted and then closed: `4401` for authentication, `1011` for
+the per-user socket cap. Messages received from DSH above `WS_MAX_MESSAGE_BYTES` (16 MiB) close
+the connection instead of being buffered; browser-to-DSH frames are bounded by uvicorn's own
+limit. Client-supplied `Forwarded`, `X-Forwarded-User`,
+`X-Auth-Request-*` and `x-forwarded-*`/`x-auth-request-*` headers are stripped on the HTTP and
+WebSocket paths; `UPSTREAM_HEADER_ALLOWLIST` replaces that denylist with an explicit allowlist.
+
## Decisions and trade-offs
### ADR-1: Starlette ASGI instead of a sync Flask proxy
@@ -279,14 +317,21 @@ sequenceDiagram
(`kubectl annotate sandboxclaim ...`) to keep being swept. Fine for a reference pattern, not
for a product.
-### ADR-6: Gateway cookie session instead of bearer tokens in the browser
+### ADR-6: Opaque server-side session cookie instead of bearer tokens in the browser
-- **Decision:** browsers get `dsh_gw_session=:`, `HttpOnly`, `SameSite=Lax`
- (`Secure` when enabled); API clients may still use `Authorization: Bearer`.
-- **Why:** tokens never appear in URLs or JS-readable storage, and `SameSite=Lax` plus a strict
- `Origin`/`Referer` check on cookie-authenticated mutations blocks basic CSRF.
-- **Cost:** cookie-based auth needs TLS and correct `COOKIE_SECURE`; there is no signed-cookie
- layer, so the gateway trusts the token hash comparison for validation.
+- **Decision:** browsers get `dsh_gw_session=`, `HttpOnly`, `SameSite=Lax` (`Secure` when
+ enabled); the sid maps to `Session(user, expires_at)` in gateway memory. API clients may still
+ use `Authorization: Bearer`. The cookie name is `__Host-dsh_gw_session` when the Secure flag
+ is on and no explicit `COOKIE_NAME` is set.
+- **Why:** tokens never appear in URLs or JS-readable storage, the cookie value is useless if
+ leaked outside the gateway session table, sessions can be revoked server-side (logout,
+ user delete, recreate, expiry), and a cookie cannot be reused as a bearer token for another
+ user's path. `SameSite=Lax` plus a strict `Origin`/`Referer` check on cookie-authenticated
+ mutations blocks basic CSRF.
+- **Cost:** stateful auth in a single process: a restart logs everyone out and horizontal
+ scaling needs a shared session store. Cookie-based auth also needs TLS and the correct
+ `COOKIE_SECURE` setting; the gateway refuses to start with `COOKIE_SECURE=false` unless
+ `ALLOW_INSECURE_COOKIES=1` is set for a local HTTP demo.
## Security and RBAC
@@ -305,33 +350,52 @@ Notes and limits:
permission is scoped to one namespace, but anyone who can read gateway memory or logs can
impersonate users. Prefer the upstream token API when available.
- **No cluster-wide access, no secret reads.** The gateway does not read `dsh-provider-keys`.
-- **Browser-facing hardening:** `HttpOnly`/`SameSite=Lax`/optional `Secure` cookies,
- timing-safe SHA-256 token comparison, strict same-origin check on cookie-auth mutations,
- request-body limit (`MAX_BODY_MB`), hop-by-hop header stripping, upstream `Set-Cookie`
- dropped, DSH `Location` headers rewritten under `/u//`.
+ (It does read the optional signup invite code if it is mounted as an env var.)
+- **Browser-facing hardening:** opaque server-side sessions instead of token-bearing cookies,
+ `HttpOnly`/`SameSite=Lax`/optional `Secure` (+ `__Host-` prefix when secure),
+ timing-safe SHA-256 bearer-token comparison, timing-safe invite-code comparison, strict
+ same-origin check on cookie-auth mutations, request-body limit (`MAX_BODY_MB`), hop-by-hop
+ header stripping, client identity/hop headers (`Forwarded`, `X-Forwarded-User`,
+ `X-Auth-Request-*`, `x-forwarded-*`/`x-auth-request-*`) stripped on HTTP and WebSocket,
+ upstream `Set-Cookie` dropped, DSH `Location` headers rewritten under `/u//`.
+- **Session lifecycle:** login/signup mint an opaque sid; logout and user delete revoke it;
+ `/recreate` revokes every session for that user; expiry (`COOKIE_MAX_AGE`, default 7 days) is
+ enforced on lookup. The table is in memory, so a restart logs browsers out and a second
+ replica needs shared state.
+- **Abuse controls (per replica):** `AUTH_RATE_LIMIT`/`AUTH_RATE_WINDOW` throttle
+ unauthenticated signup/login per client with `429` + `Retry-After`; `SIGNUP_INVITE_CODE`
+ gates signup; `MAX_IN_FLIGHT_PER_USER` caps concurrent proxied requests + WebSockets per user;
+ `WS_MAX_MESSAGE_BYTES` caps messages received from DSH; browser-to-DSH frames are bounded by
+ uvicorn. These are in-process guards, not an ingress rate limiter or audit log.
- **Host allow-list:** DSH rejects unknown `Host` headers. The gateway forwards the browser's
`Host`, so every public hostname must be in `DSH_TRUSTED_HOSTS`; `localhost`/`127.0.0.1` are
always trusted.
- **TLS is external.** The gateway serves plain HTTP; terminate TLS at the Ingress and set
- `COOKIE_SECURE=true`. Without TLS, session cookies are replayable on the network.
+ `COOKIE_SECURE=true`. The base demo sets `COOKIE_SECURE=false` with
+ `ALLOW_INSECURE_COOKIES=1` for the port-forward walkthrough; without TLS, session cookies are
+ replayable on the network and that combination must never be exposed.
- **Tenant isolation** is pod + PVC separation; all tenants share one namespace, one DSH binary,
and the shared provider keys (`dsh-provider-keys` plus the optional
- `dsh-provider-keys-extra`). `manifests/optional/network-policy/` adds an opt-in sandbox
- NetworkPolicy, and gVisor/`nodeSelector` are opt-in PodSpec fields. Add per-tenant
- credentials before calling it multi-tenant-grade.
+ `dsh-provider-keys-extra`). The base example is demo-only: production requires gVisor for the
+ sandboxes, the sandbox NetworkPolicy, the gateway egress policy and the exposure NetworkPolicy,
+ plus per-tenant credentials or an egress proxy. See the
+ [production checklist](optional-hardening.md#6-production-checklist).
- **DSH's inner file sandbox is disabled by deployment override.** The template sets
`DSH_PERMISSION_MODE=danger-full-access` because the in-container backends are unusable here:
`bubblewrap` ships in the image but is unsetuid and cannot create an unprivileged user namespace
under the pod's seccomp/capability surface, and no Landlock runner is available; the confined
executor would otherwise fail closed on every command. The pod remains the boundary, so an
- agent can read its own `DSH_HOME`. Hosts with a working backend can keep a confined mode.
+ agent can read its own `DSH_HOME`. Hosts with a working backend can keep a confined mode;
+ multi-tenant deployments must put the pod in gVisor and a NetworkPolicy.
- **Optional hardening is opt-in.** gVisor, node selection, the sandbox/exposure NetworkPolicies,
extra provider keys and failed-Pod recovery are documented in
[optional hardening](optional-hardening.md). The base manifests stay portable (no gVisor
RuntimeClass, no active node labels, no NetworkPolicy, no extra Secret required, and the
controller's default `failedPodPolicy: Retain`).
-- **No gateway rate limiting.** Signup can create claims until the cluster or warm pool is
- exhausted; put limits at the ingress for real deployments.
+- **SSO is trust-on-hop.** `AUTH_PROXY_HEADER` makes the gateway trust the proxy-asserted
+ identity; the SSO variant must remain the only path to the Service, and its NetworkPolicy is
+ required, not optional. Run [`scripts/preflight-sso.sh`](../scripts/preflight-sso.sh) after
+ applying it.
## Failure modes
@@ -340,7 +404,14 @@ Notes and limits:
| DSH log line changed or rotated away | Proxy returns `503` (missing launch token) | Fix the parser or restart the pod so the token line is re-emitted |
| Probe on `/` | Pod `CrashLoopBackOff`, `401` in probe logs | Use `/favicon.svg` for readiness/liveness (shipped templates do) |
| Warm pool exhausted | JSON signup returns `202` (state `Waking`); form signup is slow | Increase `replicas` in the warm pool |
-| Gateway restart | Idle sandboxes stop suspending; handshake cache lost | Rebuilt lazily on next request; consider a shared store for production |
+| Gateway restart | Idle sandboxes stop suspending; handshake cache and browser sessions lost | Idle clocks and handshake cache rebuild lazily; sessions do not, so browsers must sign in again after a rollout. Use a shared session store for production |
+| Gateway configured `COOKIE_SECURE=false` without `ALLOW_INSECURE_COOKIES=1` | Gateway pod exits at startup / `CrashLoopBackOff` | Intentional guard against an insecure cookie on an HTTPS deployment; set `ALLOW_INSECURE_COOKIES=1` only for the local plain-HTTP demo, or set `COOKIE_SECURE=true` |
+| Auth rate limit hit | `429` + `Retry-After` on signup/login | Per-client in-memory bucket (`AUTH_RATE_LIMIT`/`AUTH_RATE_WINDOW`); wait for the window or tune it. Behind a proxy, keying uses the rightmost `X-Forwarded-For` entry |
+| Signup invite missing/wrong | `403` before the claim is created | `SIGNUP_INVITE_CODE` is set; send the `invite` form field or `X-Signup-Invite` header |
+| Per-user concurrency cap hit | HTTP `503` + `Retry-After`, or WebSocket close `1011` | `MAX_IN_FLIGHT_PER_USER` protects the single gateway from one busy tenant; reduce concurrency or raise the cap deliberately. These are per replica |
+| Oversized WebSocket message from DSH | Connection closes | `WS_MAX_MESSAGE_BYTES` (16 MiB) is enforced on upstream messages; raise it only if DSH legitimately needs larger frames |
+| Session expiry / revocation | `401` (HTTP) or close `4401` (WebSocket) | Expected after logout, user delete, `/recreate`, `COOKIE_MAX_AGE` expiry or a gateway restart; sign in again |
+| SSO variant applied without its NetworkPolicy (or with an Ingress) | Gateway accepts spoofed `X-Forwarded-User` | Demo-only configuration; keep `75-networkpolicies.yaml`, remove the second path, and run `scripts/preflight-sso.sh` |
| Browser aborts a request (tab reload, back navigation) during a slow wake or handshake | No response reaches the client; nothing appears in the access log | If the abort lands before the response starts, the body read raises `ClientDisconnect` and the app answers `499` (`CLIENT_CLOSED_REQUEST`), which cannot reach the departed peer. Nothing reached DSH, no ERROR traceback is logged, and the in-flight slot is released. A request already streaming ends with its upstream status. |
| DSH auth cookie expired mid-session | Proxy `401` once, then recovers | Gateway refreshes the handshake once |
| Upstream pod name changed after resume | Cached cookie keyed by old pod UID is useless | Cache is invalidated on suspend; next request re-handshakes |
@@ -372,10 +443,11 @@ single-retry refresh are the supported workaround.
## Evolution path
1. Replace log scraping with the upstream token API (above), deleting the `pods/log` permission.
-2. Move idle clocks, in-flight tracking, and handshake cache to a shared store; then scale the
- gateway horizontally (sticky routing not required if state is shared).
+2. Move idle clocks, in-flight tracking, handshake cache **and sessions** to a shared store; then
+ scale the gateway horizontally (sticky routing not required if state is shared).
3. Add per-tenant API keys or an egress proxy so a single key is not shared by all sandboxes.
-4. Add NetworkPolicies (gateway → sandboxes only), TLS everywhere, rate limits, and audit
- logging at the ingress.
+4. Add NetworkPolicies (gateway → sandboxes only, plus the documented gateway egress policy),
+ gVisor for sandbox pods, TLS everywhere, ingress-level rate limits/quota and audit logging.
+ The gateway's in-process limits are not a substitute.
5. If DSH grows absolute URLs or per-user cookies that cannot be rewritten, switch to
per-user subdomains with wildcard DNS/TLS.
diff --git a/examples/dsh-agents-as-a-service/docs/optional-hardening.md b/examples/dsh-agents-as-a-service/docs/optional-hardening.md
index b007f9f..7ad8137 100644
--- a/examples/dsh-agents-as-a-service/docs/optional-hardening.md
+++ b/examples/dsh-agents-as-a-service/docs/optional-hardening.md
@@ -2,14 +2,26 @@
The base example is deliberately portable: it runs on kind and GKE without a
gVisor runtime, a tainted node pool or a NetworkPolicy-capable CNI, and it
-boots with only the required `dsh-provider-keys` Secret. This document covers
-four **opt-in** controls plus failed-Pod recovery:
+boots with only the required `dsh-provider-keys` Secret. That portability is
+exactly why the base manifests are **demo-only**. This document covers the
+controls you add for production and the steps to enable or roll them back.
+
+**Production is not the base walkthrough.** At minimum, a production deployment
+needs: gVisor for the sandbox pods (or another kernel-isolation boundary), the
+sandbox NetworkPolicy, the gateway egress policy, the exposure variant's
+NetworkPolicy, TLS with `COOKIE_SECURE=true` (and no
+`ALLOW_INSECURE_COOKIES`), SSO (or an equivalent authenticating proxy) as the
+only path to the gateway, and per-tenant provider credentials/egress. The
+checklist at the end summarizes this; without the network and kernel layers the
+example is not tenant-isolated.
+
+This document covers four **opt-in** controls plus failed-Pod recovery:
| Setting | Where it lives | What it changes |
| --- | --- | --- |
-| gVisor | commented `runtimeClassName` in the SandboxTemplates and Deployments | puts DSH and/or the gateway in a userspace kernel |
+| gVisor | commented `runtimeClassName` in the SandboxTemplates and Deployments | puts DSH and/or the gateway in a userspace kernel; required for untrusted agents in production |
| nodeSelector | commented `nodeSelector` in the same files | pins pods to a chosen node pool |
-| NetworkPolicy | `manifests/optional/network-policy/`, `manifests/exposure/*/` | limits sandbox, gateway and proxy connectivity |
+| NetworkPolicy | `manifests/optional/network-policy/`, `manifests/exposure/*/`, plus the gateway egress recipe | limits sandbox, gateway and proxy connectivity; the sandbox and gateway-egress policies are required for production |
| Provider keys | optional `dsh-provider-keys-extra` Secret | injects extra provider API keys into every sandbox |
| Failed-Pod recovery | commented `failedPodPolicy` in the SandboxTemplates | replaces a Failed sandbox Pod instead of parking the sandbox |
@@ -234,10 +246,17 @@ kubectl -n dsh-demo patch sandboxtemplate dsh-web --type merge \
The base example does **not** manage NetworkPolicy: the SandboxTemplate is
`networkPolicyManagement: Unmanaged`, and the gateway has no policy. That keeps
-the walkthrough portable and independent of the sandbox-router; the opt-in
-policies below are standard Kubernetes `NetworkPolicy` objects.
+the walkthrough portable and independent of the sandbox-router. **For
+production this is not optional**: without the sandbox policy every tenant can
+reach every other tenant and cluster-internal endpoints, and without a gateway
+egress policy the gateway can talk to anything the cluster network allows.
+Apply both. The policies below are standard Kubernetes `NetworkPolicy` objects.
-### Optional sandbox policy
+> **Demo vs production.** An installation that skips the sandbox NetworkPolicy
+> and the gateway egress policy is a demo. Exposure (Ingress/Gateway API) adds
+> ingress protection only; it does not isolate tenants from each other.
+
+### Sandbox policy (required for production)
[`manifests/optional/network-policy/10-sandbox-networkpolicy.yaml`](../manifests/optional/network-policy/10-sandbox-networkpolicy.yaml)
selects every Sandbox pod through the agent-sandbox tracking label
@@ -301,10 +320,10 @@ Caveats:
- `kubectl port-forward` still works for debugging (the connection is local to
the pod), except under gVisor.
-### Exposure-variant policies
+### Exposure-variant policies (required, part of the variant)
-Each exposure variant ships its own policy. They are part of the variant, not a
-separate opt-in step:
+Each exposure variant ships its own policy. They are part of the variant and
+**required for production**; an exposure without its policy is demo-only:
- [`manifests/exposure/ingress/61-networkpolicy.yaml`](../manifests/exposure/ingress/61-networkpolicy.yaml)
allows only Traefik pods (`app.kubernetes.io/name: traefik`) to reach the
@@ -317,7 +336,10 @@ separate opt-in step:
The SSO policy is what turns the trust boundary in the
[exposure README](../manifests/exposure/README.md) into network enforcement.
The `dsh-gateway` Service must still stay ClusterIP; do not apply both exposure
-variants.
+variants. After applying the SSO variant, run
+[`scripts/preflight-sso.sh`](../scripts/preflight-sso.sh): it checks the
+gateway's SSO env, the Secrets, the TLS reference, leftover placeholders and
+accidental second paths (such as an Ingress).
Verify the negative case from a sandbox pod (it is not the proxy):
@@ -332,11 +354,12 @@ different pod label, replace the `podSelector` with the correct selector or an
`ipBlock`. As with the sandbox policy, kubelet probes can be affected on some
CNIs.
-### Gateway egress (documented, not shipped)
+### Gateway egress (required for production)
-The gateway needs DNS, the Kubernetes API, and TCP `3080` to sandbox pods. The
-API server endpoint is cluster-specific (Service CIDR or node IP/port), so an
-egress policy is **not** shipped. If your cluster has a stable API-server CIDR
+The gateway needs DNS, the Kubernetes API, and TCP `3080` to sandbox pods. An
+egress policy is **required for production** but is not shipped, because the API
+server endpoint is cluster-specific (Service CIDR or node IP/port) and a wrong
+value silently breaks signup/proxy. If your cluster has a stable API-server CIDR
and port, add an egress policy like this (replace the placeholders; verify the
port, commonly `443` or `6443`):
@@ -597,7 +620,63 @@ done
Warm-pool spares pick the default up as they are claimed and replenished, or
delete them as in step 2 above.
-## 6. Verification summary
+## 6. Production checklist
+
+The base walkthrough is a demo. A production install must satisfy every item
+below; the sections above give the commands and caveats.
+
+**Isolation and network**
+
+- [ ] Sandbox pods run under gVisor (or an equivalent kernel-isolation
+ boundary) because DSH runs with `DSH_PERMISSION_MODE=danger-full-access`
+ in this image; the pod is the boundary. (Section 1.)
+- [ ] The sandbox NetworkPolicy is applied and verified: no tenant→tenant,
+ no tenant→cluster-Service/API, DNS + public HTTPS only. (Section 3.)
+- [ ] The gateway egress policy is applied with the cluster's real API-server
+ CIDR/port, scoped to DNS, the API server and sandbox pods on `3080`.
+ (Section 3, "Gateway egress".)
+- [ ] The exposure variant's NetworkPolicy is applied. An exposure without it is
+ demo-only. For SSO, the gateway is reachable **only** through
+ oauth2-proxy; there is no second Ingress/Gateway/port-forward path.
+- [ ] Nodes/CNI enforce NetworkPolicy (verify the negative cases, not just the
+ positive path).
+- [ ] The gateway and (optionally) oauth2-proxy run with `runAsNonRoot`,
+ dropped capabilities, `RuntimeDefault` seccomp and a read-only root
+ filesystem, as shipped.
+
+**Authentication, TLS and abuse controls**
+
+- [ ] TLS terminates in front of the gateway, `COOKIE_SECURE=true`, and
+ `ALLOW_INSECURE_COOKIES` is **not** set. The gateway refuses to start with
+ an insecure cookie unless explicitly acknowledged.
+- [ ] SSO (or an equivalent authenticating proxy) is enabled with
+ `AUTH_PROXY_HEADER=X-Forwarded-User`, and
+ [`scripts/preflight-sso.sh`](../scripts/preflight-sso.sh) exits `0`.
+- [ ] No `your-github-org` / `dsh-demo.example.com` placeholder remains.
+- [ ] `SIGNUP_INVITE_CODE` is set from a Secret if the gateway's own signup form
+ is reachable, and `AUTH_RATE_LIMIT` is set deliberately (`30`/60s shipped).
+ Add ingress-level rate limiting/quota too; the gateway's buckets are
+ in-process and reset on restart.
+- [ ] Session lifetime (`COOKIE_MAX_AGE`) and the logout/delete revocation
+ behavior are acceptable; account for gateway restarts logging users out.
+- [ ] `MAX_IN_FLIGHT_PER_USER` and `WS_MAX_MESSAGE_BYTES` fit the workload.
+
+**Credentials, platform and operations**
+
+- [ ] Provider keys are per-tenant (or routed through an egress proxy) instead
+ of the shared `dsh-provider-keys` Secrets; any shared key is treated as
+ visible to every tenant.
+- [ ] RBAC stays namespace-scoped; `pods/log` is treated as sensitive because
+ it contains launch tokens and model output. Plan the upstream token API
+ migration.
+- [ ] Images are digest-pinned and dependency installs are hash-locked.
+- [ ] Gateway replicas stay at `1` (in-memory sessions/idle state) or the state
+ is externalized.
+- [ ] `failedPodPolicy: Recreate` is set if node eviction/spot reclaim is
+ expected.
+- [ ] Audit logging, alerting and backups of the PVCs are in place.
+
+## 7. Verification summary
Required locally for any change to this example:
@@ -605,6 +684,7 @@ Required locally for any change to this example:
make check # versions, example layout, manifest YAML shape
yamllint .
make test # only when the gateway code changed
+shellcheck examples/dsh-agents-as-a-service/scripts/*.sh
```
CI additionally parses every manifest and runs `kubectl apply
@@ -618,17 +698,25 @@ Manual checks, on a cluster with the relevant CNI/runtime:
gateway proxy works, port-forward limitation acknowledged.
- nodeSelector: pods land on the intended nodes; no `Pending` events.
- Sandbox policy: the allow/deny matrix above.
+- Gateway egress policy: signup and proxy still work; the gateway cannot reach
+ an arbitrary cluster Service or the metadata server.
- Exposure policies: browser path works; a non-proxy pod cannot reach the
- gateway; probes stay healthy.
+ gateway; probes stay healthy. `scripts/preflight-sso.sh` exits `0`.
+- Cookie/TLS guard: with `COOKIE_SECURE=false` and no
+ `ALLOW_INSECURE_COOKIES` the gateway refuses to start; with TLS it starts and
+ the cookie is `Secure` (`__Host-` prefixed).
+- Auth abuse controls: `429` + `Retry-After` after the configured limit, `403`
+ on a wrong invite code, and revocations on logout/delete/recreate.
- Provider keys: set/unset output (never the values) and the refresh procedure.
- Failed-Pod recovery: `spec.failedPodPolicy` on the Sandbox, and the drill
above replaces a `Failed` Pod while keeping the claim and PVC.
-## 7. Rollback summary
+## 8. Rollback summary
| Layer | Rollback |
| --- | --- |
| Sandbox policy | `kubectl delete -f manifests/optional/network-policy/` |
+| Gateway egress policy | `kubectl delete networkpolicy dsh-gateway-egress -n dsh-demo` (do not leave it half-configured) |
| Exposure policy | delete the exposure variant directory |
| gVisor | patch `runtimeClassName: null`, recreate pods |
| nodeSelector | patch `nodeSelector: null`, recreate pods |
diff --git a/examples/dsh-agents-as-a-service/gateway/Dockerfile b/examples/dsh-agents-as-a-service/gateway/Dockerfile
index 6a6fd2d..e4d5fd2 100644
--- a/examples/dsh-agents-as-a-service/gateway/Dockerfile
+++ b/examples/dsh-agents-as-a-service/gateway/Dockerfile
@@ -1,16 +1,73 @@
-FROM python:3.12-slim
+# syntax=docker/dockerfile:1.7
+#
+# Example gateway image: the tenant-facing proxy in front of DSH sandboxes.
+# The base image is digest-pinned in versions.env (GATEWAY_PYTHON_IMAGE) and
+# scripts/check-versions.sh verifies the ARG default below.
+#
+# Two stages on that base:
+# build installs the fully hash-pinned runtime dependencies into /venv as
+# a dedicated non-root UID, so the runtime stage copies a venv that
+# contains no root-owned files.
+# runtime copies /venv and the app, runs as UID 10000 with HOME=/tmp and no
+# writable location of its own: usable with readOnlyRootFilesystem.
+ARG GATEWAY_PYTHON_IMAGE=docker.io/python:3.12-slim@sha256:aa943ce411423c7582288f18707ff66eddcbaeb510502d747a72aeb34f30d159
-# Read-only-rootfs friendly: no .pyc writes, no home directory writes.
+# ---------------------------------------------------------------------------
+# Build stage: hash-pinned virtualenv owned by the runtime identity.
+# ---------------------------------------------------------------------------
+FROM $GATEWAY_PYTHON_IMAGE AS build
+
+ARG APP_UID=10000
+ARG APP_GID=10000
+
+# No .pyc writes and no user cache dirs: the runtime image stays read-only.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1 \
HOME=/tmp
-WORKDIR /app
+RUN groupadd --gid "$APP_GID" app \
+ && useradd --uid "$APP_UID" --gid "$APP_GID" --no-create-home \
+ --home-dir /tmp --shell /usr/sbin/nologin app \
+ && python -m venv /venv \
+ && chown -R "$APP_UID:$APP_GID" /venv
+
+WORKDIR /build
COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-COPY gateway.py .
+
+# Install as the unprivileged user so every file in /venv is owned by UID 10000.
+# --require-hashes makes pip reject any requirement without a matching hash:
+# the dependency set in requirements.txt is fully pinned and verified. Use the
+# venv's own pip: the system pip would user-install into $HOME instead.
+USER $APP_UID:$APP_GID
+RUN /venv/bin/pip install --no-cache-dir --require-hashes -r requirements.txt
+
+# ---------------------------------------------------------------------------
+# Runtime stage
+# ---------------------------------------------------------------------------
+FROM $GATEWAY_PYTHON_IMAGE AS runtime
+
+ARG APP_UID=10000
+ARG APP_GID=10000
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ HOME=/tmp \
+ PATH="/venv/bin:$PATH"
+
+RUN groupadd --gid "$APP_GID" app \
+ && useradd --uid "$APP_UID" --gid "$APP_GID" --no-create-home \
+ --home-dir /tmp --shell /usr/sbin/nologin app
+
+WORKDIR /app
+
+# Dependencies come from the build stage: same interpreter, no build tools left
+# in the runtime image and nothing owned by root inside /venv.
+COPY --from=build --chown=$APP_UID:$APP_GID /venv /venv
+COPY --chown=$APP_UID:$APP_GID gateway.py .
EXPOSE 8080
+
# Run as a fixed non-root UID; HOME=/tmp keeps caches out of the image.
USER 10000
diff --git a/examples/dsh-agents-as-a-service/gateway/README.md b/examples/dsh-agents-as-a-service/gateway/README.md
index 8a91504..043f1ba 100644
--- a/examples/dsh-agents-as-a-service/gateway/README.md
+++ b/examples/dsh-agents-as-a-service/gateway/README.md
@@ -10,11 +10,13 @@ It does four things:
(only its SHA-256 is stored as a claim annotation), reports derived state,
and deletes the claim (cascading the sandbox and PVC).
2. **Browser sessions** — exchanges a user token for an HttpOnly session
- cookie so the DSH UI works in a browser at `/u//`. Automation can
- still use `Authorization: Bearer `.
+ cookie whose value is only an opaque, server-side session id (user and
+ expiry stay in the gateway's memory) so the DSH UI works in a browser at
+ `/u//`. Automation can still use
+ `Authorization: Bearer `, which never comes from the cookie.
3. **Reverse proxy** — proxies HTTP (including SSE) and WebSocket traffic to
the sandbox's DSH Web server on `:3080`, including DSH's own cookie
- handshake.
+ handshake. Client-supplied forwarding/identity headers are stripped.
4. **Optional SSO** — when `AUTH_PROXY_HEADER` is set, trusts the identity
header an authenticating reverse proxy (for example oauth2-proxy) forwards,
auto-provisions that user, and exposes `POST /recreate` for a clean restart.
@@ -63,10 +65,10 @@ token. A failed handshake returns `503` with `Retry-After: 10`.
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| `GET` | `/healthz` | none | Liveness/readiness. Never touches the cluster. |
-| `GET` | `/` | none | Landing page with signup + token login forms. With SSO it auto-provisions the proxy-identified user and redirects to their workspace. |
-| `POST` | `/users` | none | Signup. JSON body `{"user": ""}` or form field `user`. JSON returns `201` when Ready, `202` when accepted but still Waking; form returns `303` to `/u//`. Both set the session cookie. `409` if the user exists, `400` if invalid, `413` if the body is too large. |
-| `POST` | `/login` | none | Form (`user`, `token`) returns `303` to `/u//`; JSON returns `200`. Sets the session cookie. `401` on a bad token, `404` for an unknown user. |
-| `POST` | `/logout` | none | Clears the session cookie and `303`s to `/`. |
+| `GET` | `/` | none | Landing page with signup + token login forms (plus an invite-code input when `SIGNUP_INVITE_CODE` is set). With SSO it auto-provisions the proxy-identified user and redirects to their workspace. |
+| `POST` | `/users` | none | Signup. JSON body `{"user": ""}` or form field `user`; when `SIGNUP_INVITE_CODE` is set, an `invite` form field or `X-Signup-Invite` header must match it (`403` otherwise, checked before the user lookup). JSON returns `201` when Ready, `202` when accepted but still Waking; form returns `303` to `/u//`. Both set the session cookie. `409` if the user exists, `400` if invalid, `413` if the body is too large, `429` when the auth rate limit is hit. |
+| `POST` | `/login` | none | Form (`user`, `token`) returns `303` to `/u//`; JSON returns `200`. Sets the session cookie. `401` on a bad token **and** for an unknown user (no username enumeration). `429` when the auth rate limit is hit. Both auth routes reject a form post whose `Origin`/`Referer` contradicts `Host` (`403`) when the browser sent one. |
+| `POST` | `/logout` | none | Revokes the server-side session, clears the cookie and `303`s to `/`. |
| `POST` | `/recreate` | trusted proxy header | **SSO only.** Deletes the authenticated user's claim (cascading sandbox + PVC, so workspace data is wiped), waits out finalizers, provisions a fresh claim and `303`s to `/u//`. `401` without a valid identity header; cookie-authenticated calls must be same-origin. |
| `GET` | `/users/{user}` | bearer or cookie | Derived state: `Provisioning`, `Waking`, `Ready`, `Suspending`, `Suspended`. |
| `DELETE` | `/users/{user}` | bearer or cookie | Delete the claim (sandbox + PVC cascade). Cookie-auth mutations require a same-origin `Origin`/`Referer`. |
@@ -78,19 +80,49 @@ token. A failed handshake returns `503` with `Retry-After: 10`.
* `Authorization: Bearer ` is checked with a timing-safe SHA-256
comparison against the `aaas.example.com/token-sha256` claim annotation.
-* The browser session cookie is
- `dsh_gw_session=:` (HttpOnly, SameSite=Lax, Path=/, optional
- Secure). A session for user A never authorizes user B.
+* The browser session cookie is **opaque**: its value is a random
+ server-side session id; the user and expiry live in gateway memory
+ (`GatewayState.sessions`). A cookie can never be edited into another user's
+ credentials, a session id is not accepted as a bearer token, and a bearer
+ token never authorizes a different user. The cookie name defaults to
+ `__Host-dsh_gw_session` when `COOKIE_SECURE=true` (HTTPS) and to
+ `dsh_gw_session` for plain HTTP; attributes are HttpOnly, SameSite=Lax,
+ Path=/ and (with `COOKIE_SECURE`) Secure.
+* Sessions are revoked on `POST /logout`, on `DELETE /users/{user}`, on
+ `POST /recreate`, and when they expire (looked up on every request; the
+ sweeper also drops expired entries). `from_env()` refuses to start without
+ `COOKIE_SECURE=true` or the explicit `ALLOW_INSECURE_COOKIES=1` opt-out for
+ local HTTP.
* Cookie-authenticated **mutations** (`POST`, `PUT`, `PATCH`, `DELETE`) and
cookie-authenticated **WebSocket upgrades** require the request `Origin`
(or `Referer` fallback) to match the `Host` header. Missing or mismatched
cross-origin metadata is rejected (`403` HTTP, close code `4401` for WS).
+ Form signup/login additionally reject a cross-origin `Origin`/`Referer`
+ when the browser sent one, before the body is read.
+* `POST /users` and `POST /login` are rate limited per client:
+ `AUTH_RATE_LIMIT` attempts per `AUTH_RATE_WINDOW` seconds (default 30/60,
+ `0` disables). The client key is the rightmost `X-Forwarded-For` entry when
+ present (the address the last trusted proxy saw, not a spoofable leftmost
+ value), else the socket peer. Over-limit requests get `429` +
+ `Retry-After` before the body is read.
+* Proxy requests strip client-supplied forwarding/identity headers in both
+ HTTP and WebSocket paths: `Forwarded`, every `X-Forwarded-*`
+ (including `X-Forwarded-User`), every `X-Auth-Request-*`, and `X-Real-IP`.
+ The gateway sets its own `X-Forwarded-For/Host/Proto/Port` values from the
+ connection. `UPSTREAM_HEADER_ALLOWLIST` optionally narrows the forwarded
+ client headers further (comma-separated, case-insensitive); unset keeps the
+ denylist behavior, and the gateway's own values are always added.
* The gateway's bearer token, the browser `Cookie` header and any `?token=`
query parameter are never forwarded upstream. Only the cached `dsh-auth-*`
cookie is injected.
* Upstream `Set-Cookie` headers are dropped. `Location` headers that are
absolute paths are rewritten under `/u//`, and any `token` query
parameter is stripped.
+* Concurrency is capped per user: `MAX_IN_FLIGHT_PER_USER` proxied requests
+ (default 32) before the gateway answers `503` + `Retry-After` (HTTP) or
+ closes the WebSocket with `1011`. Messages received from DSH are limited to
+ `WS_MAX_MESSAGE_BYTES` (default 16 MiB); browser-to-DSH frames are bounded by
+ uvicorn's own WebSocket size limit.
### SSO (opt-in)
@@ -106,7 +138,8 @@ kubectl -n dsh-demo set env deploy/dsh-gateway AUTH_PROXY_HEADER=X-Forwarded-Use
When set, the gateway treats a request carrying that header as authenticated as
that user, provided the value is a valid DNS-1123 label; the header is never
trusted when the variable is unset. The landing page then auto-provisions the
-user (creates their claim idempotently, mints a rotated session token) and
+user (creates their claim idempotently, rotates the claim token when a new
+session is needed and mints an opaque session cookie) and
redirects to `/u//`, and `POST /recreate` becomes available for a clean
restart. The base manifests leave it unset on purpose; the
[SSO exposure variant](../manifests/exposure/gateway-api-sso/) documents how to
@@ -133,14 +166,19 @@ oauth2-proxy's egress to the gateway, DNS and public HTTPS.
| --- | --- |
| Unknown user / claim | `404` |
| Missing or invalid credentials | `401` |
-| Cross-origin cookie mutation | `403` |
+| Unknown user on `/login` | `401` (same as a bad token; no username enumeration) |
+| Cross-origin cookie mutation / cross-origin form signup or login | `403` |
+| Auth route over `AUTH_RATE_LIMIT` | `429` + `Retry-After`, before the body is read |
| Request body over `MAX_BODY_MB` | `413` |
| Client aborts mid-body, before the response starts | `499` (`CLIENT_CLOSED_REQUEST`). Nothing is forwarded to DSH and nothing can be delivered; it is not an error, so no traceback is logged. Covers the body-reading control routes (`POST /users`, `POST /login`) as well as the proxy. |
| Client aborts mid-stream (SSE, long response) | The response already started, so its upstream status stands (typically `200`); the stream simply ends. No traceback, and the in-flight slot is released as the relay unwinds. |
-| Sandbox not provisioned, wake timeout, handshake failure | `503` + `Retry-After: 10` |
+| Sandbox not provisioned, wake timeout, handshake failure | `503` + `Retry-After: 10`. The handshake error body is a fixed message: it never contains the launch token, the handshake URL or pod/credential details. |
+| User over `MAX_IN_FLIGHT_PER_USER` | `503` + `Retry-After` (HTTP); the existing in-flight requests are untouched |
| Upstream connection/protocol failure | `502` |
| WS auth failure (unknown user, bad token, cross-origin) | accept, then close `4401` |
+| WS per-user concurrency cap | accept, then close `1011` |
| WS upstream/handshake failure | accept, then close `1011` |
+| WS message from DSH over `WS_MAX_MESSAGE_BYTES` | the upstream/relay closes the connection (websockets' message-too-big path) |
A close code can only be delivered after the WebSocket upgrade is accepted;
Starlette would turn an early close into an HTTP 403, which is why WS auth
@@ -166,17 +204,27 @@ can also be injected into `create_app` for tests.
| `DSH_PORT` | `3080` | Upstream DSH port. |
| `DSH_CONTAINER` | `dsh-web` | Container whose logs carry the launch token. |
| `DSH_LOG_TAIL_LINES` | `2000` | How many log lines to scan for the launch token. |
-| `COOKIE_NAME` | `dsh_gw_session` | Browser session cookie name. |
-| `COOKIE_SECURE` | `false` | Add `Secure` to the session cookie when served over TLS. |
-| `COOKIE_MAX_AGE` | `604800` | Session cookie lifetime in seconds. |
+| `COOKIE_NAME` | `__Host-dsh_gw_session` when secure, else `dsh_gw_session` | Browser session cookie name. Only an opaque session id is stored in it. |
+| `COOKIE_SECURE` | `false` | Add `Secure` to the session cookie. Required unless `ALLOW_INSECURE_COOKIES=1` is set for local HTTP. |
+| `ALLOW_INSECURE_COOKIES` | `0` | Explicit opt-out for plain-HTTP demos. Without it, `Settings.from_env()` refuses to start unless `COOKIE_SECURE=true`. |
+| `COOKIE_MAX_AGE` | `604800` | Session lifetime in seconds (server-side expiry and cookie `Max-Age`). |
| `MAX_BODY_MB` | `32` | Maximum buffered request body (and `413` threshold). |
| `HTTP_TIMEOUT` | `300` | Upstream connect/read timeout in seconds. Proxied streams (SSE, long responses) keep the connect/write budget but have **no read timeout**, so an idle event stream is not cut every five minutes. |
-| `SWEEP_INTERVAL` | `15` | Seconds between idle sweeps. |
+| `SWEEP_INTERVAL` | `15` | Seconds between idle sweeps. Sweeps also prune expired sessions and rate buckets and orphaned per-user state. |
+| `SIGNUP_INVITE_CODE` | unset | When set, `POST /users` requires the matching `invite` form field or `X-Signup-Invite` header (timing-safe compare); the landing page then shows an invite input. Unset leaves signup open. |
+| `AUTH_RATE_LIMIT` | `30` | Attempts allowed per client per `AUTH_RATE_WINDOW` on `POST /users` and `POST /login`; `0` disables the limit. |
+| `AUTH_RATE_WINDOW` | `60` | Rate-limit window in seconds. |
+| `UPSTREAM_HEADER_ALLOWLIST` | unset | Comma-separated, case-insensitive allowlist of client headers to forward upstream. Unset keeps the denylist behavior; forwarding/identity headers are stripped in both modes. |
+| `MAX_IN_FLIGHT_PER_USER` | `32` | Concurrent proxied requests per user before `503`/WS `1011`. `0` disables the cap. |
+| `WS_MAX_MESSAGE_BYTES` | `16777216` | Largest WebSocket message (16 MiB) accepted from DSH. Browser-to-DSH frames are bounded by uvicorn's own limit. |
| `AUTH_PROXY_HEADER` | unset | Opt-in SSO: name of the trusted identity header set by an authenticating reverse proxy (e.g. `X-Forwarded-User`). Unset disables SSO and the header is never trusted. Enable only when the Service is reachable exclusively through that proxy (see [SSO](#sso-opt-in)). |
The shipped `manifests/50-gateway.yaml` sets `IDLE_TIMEOUT=360` and
`WAKE_TIMEOUT=240` for the demo; the e2e script overrides them to 60/120 so the
-smoke test does not wait minutes for the sweeper.
+smoke test does not wait minutes for the sweeper. It also sets
+`COOKIE_SECURE=false` together with the explicit `ALLOW_INSECURE_COOKIES=1`
+acknowledgement because the demo is reached over plain HTTP (a `Secure` cookie
+would never be sent). HTTPS deployments must set `COOKIE_SECURE=true`.
### Required RBAC
@@ -197,8 +245,10 @@ python -m pip install -r requirements-dev.txt
python -m pytest -q
python -m ruff check .
-# Uvicorn factory (loads in-cluster credentials only on the first claim call)
-uvicorn gateway:create_app --factory --host 0.0.0.0 --port 8080
+# Uvicorn factory (loads in-cluster credentials only on the first claim call).
+# from_env() insists on an explicit acknowledgement for plain HTTP; behind TLS
+# set COOKIE_SECURE=true instead.
+ALLOW_INSECURE_COOKIES=1 uvicorn gateway:create_app --factory --host 0.0.0.0 --port 8080
```
Container image, from the repository root:
@@ -226,18 +276,27 @@ uvicorn's access log so bearer tokens in URLs are not logged.
Log rotation, a very chatty container, or DSH changing its startup message
breaks the handshake. Prefer a first-class token/credential API when DSH
offers one.
-* **Single replica, in-memory state.** The handshake cookie cache, idle
- activity clocks and in-flight counters live in one process. Running more
- than one replica would need shared state (or sticky routing), otherwise each
- replica handshakes independently and the sweeper may race another replica.
+* **Single replica, in-memory state.** The handshake cookie cache, the opaque
+ session table, idle activity clocks, in-flight counters and rate-limit
+ buckets live in one process. Running more than one replica would need shared
+ state (or sticky routing), otherwise each replica handshakes independently,
+ the sweeper may race another replica, and a session/rate budget is only seen
+ by the replica that created it.
+* **Sessions die with the process.** An opaque session id is valid only in the
+ process that minted it and expires after `COOKIE_MAX_AGE`; a restart logs
+ every browser out (tokens are unaffected).
* **In-memory token cache has no TTL.** It is keyed by pod UID, so pod
restarts invalidate it naturally; otherwise entries survive until the pod or
claim is deleted.
-* **No TLS termination.** Run behind an ingress/load balancer; set
- `COOKIE_SECURE=true` there.
+* **No TLS termination.** Run behind an ingress/load balancer and set
+ `COOKIE_SECURE=true`. `Settings.from_env()` refuses to start without it
+ unless `ALLOW_INSECURE_COOKIES=1` explicitly acknowledges plain-HTTP
+ sessions.
* **Origin-based CSRF protection only.** Cookie-auth mutations require a
- same-origin `Origin`/`Referer`; there is no per-form CSRF token, rate
- limiting or login throttling.
+ same-origin `Origin`/`Referer`, and form signup/login reject a contradicting
+ origin when the browser sends one; there is no per-form CSRF token. Auth
+ routes have per-client rate limiting and login throttling, but the budget is
+ in-process (per replica) and keyed by the rightmost `X-Forwarded-For` value.
* **HTTP request bodies are buffered** up to `MAX_BODY_MB`; uploads are not
streamed. Responses (including SSE) are streamed.
* **Upstream cookies are not forwarded.** DSH cannot rotate its own
diff --git a/examples/dsh-agents-as-a-service/gateway/gateway.py b/examples/dsh-agents-as-a-service/gateway/gateway.py
index b3f7432..30fbfb1 100644
--- a/examples/dsh-agents-as-a-service/gateway/gateway.py
+++ b/examples/dsh-agents-as-a-service/gateway/gateway.py
@@ -7,9 +7,12 @@ single teaching-friendly module with three jobs:
(only its SHA-256 is stored, as a claim annotation), report derived state,
and delete the claim (which cascades the sandbox and its PVC).
* **Browser sessions** -- exchange a user token for an HttpOnly session cookie
- so the DSH UI can be opened in a browser at ``/u//``. Browser requests
- authenticate with that cookie; automation can keep using
- ``Authorization: Bearer ``.
+ whose value is an opaque, server-side session id (the user and expiry live in
+ memory) so the DSH UI can be opened in a browser at ``/u//``. Browser
+ requests authenticate with that cookie; automation can keep using
+ ``Authorization: Bearer ``. Sessions are revoked on logout, user
+ deletion, recreate and expiry, and a cookie is never accepted as a bearer
+ token (nor the other way around).
* **Reverse proxy** -- HTTP (including SSE) and WebSocket proxying to the
sandbox's DSH Web server on ``:3080``. DSH itself is cookie-gated: the
gateway scrapes the launch token from the pod logs, performs the ``?token=``
@@ -38,6 +41,7 @@ import contextlib
import hashlib
import html
import json
+import math
import os
import re
import secrets
@@ -121,6 +125,19 @@ OWNED_REQUEST_HEADERS = HOP_BY_HOP | {
"x-real-ip",
}
+# Default session cookie names. ``__Host-`` is only legal over HTTPS and pins
+# the cookie to Path=/ with no Domain attribute; the insecure default keeps the
+# example usable over plain HTTP behind a port-forward.
+DEFAULT_COOKIE_NAME = "dsh_gw_session"
+SECURE_COOKIE_NAME = "__Host-dsh_gw_session"
+
+# Header prefixes a client must never be able to smuggle upstream. The gateway
+# derives its own X-Forwarded-* values from the connection; anything the browser
+# sends with these names is attacker-controlled and is dropped in both the HTTP
+# and WebSocket proxy paths.
+FORWARDING_HEADER_PREFIXES = ("x-forwarded-", "x-auth-request-")
+FORWARDING_HEADERS = frozenset({"forwarded"})
+
_DSH_WEB_LINE = re.compile(r"dsh web:\s+(?Phttps?://\S+)")
@@ -249,6 +266,33 @@ def same_origin(headers) -> bool:
return False
+def strip_forwarding_header(name: str) -> bool:
+ """True for client-supplied identity/forwarding headers we never forward.
+
+ ``X-Forwarded-User`` and ``X-Auth-Request-*`` carry identities an
+ authenticating proxy or the gateway itself owns; ``Forwarded`` /
+ ``X-Forwarded-*`` describe the hop chain. Forwarding any of them lets a
+ browser spoof its origin or a trusted SSO user upstream.
+ """
+ lowered = name.lower()
+ return lowered in FORWARDING_HEADERS or lowered.startswith(FORWARDING_HEADER_PREFIXES)
+
+
+def client_identity(headers, client) -> str:
+ """The rate-limit bucket for a request.
+
+ The rightmost ``X-Forwarded-For`` entry is the address the last trusted
+ proxy saw (each proxy appends), so it cannot be spoofed by prepending
+ arbitrary values. Without the header fall back to the socket peer.
+ """
+ forwarded = (headers.get("x-forwarded-for") or "").strip()
+ if forwarded:
+ parts = [part.strip() for part in forwarded.split(",") if part.strip()]
+ if parts:
+ return parts[-1]
+ return getattr(client, "host", None) or "unknown"
+
+
def rewrite_location(location: str, user: str) -> str:
"""Prefix absolute-path redirects so the browser stays under ``/u//``."""
if not location.startswith("/") or location.startswith("//"):
@@ -306,6 +350,14 @@ def _env_float(name: str, default: float) -> float:
return default
+def _env_allowlist(name: str) -> frozenset[str] | None:
+ """Parse a comma-separated header allowlist; unset means "denylist mode"."""
+ raw = os.environ.get(name)
+ if raw is None:
+ return None
+ return frozenset(part.strip().lower() for part in raw.split(",") if part.strip())
+
+
# --- Settings --------------------------------------------------------------
@@ -325,7 +377,7 @@ class Settings:
dsh_port: int = 3080
dsh_container: str = "dsh-web"
dsh_log_tail_lines: int = 2000
- cookie_name: str = "dsh_gw_session"
+ cookie_name: str = DEFAULT_COOKIE_NAME
cookie_secure: bool = False
cookie_max_age: int = 7 * 24 * 60 * 60
max_body_bytes: int = 32 * 1024 * 1024
@@ -339,9 +391,36 @@ class Settings:
# valid user in this header are treated as authenticated (SSO); unset (None)
# disables SSO entirely and the header is never trusted.
auth_proxy_header: str | None = None
+ # Abuse controls. AUTH_RATE_LIMIT applies to the unauthenticated auth routes
+ # (POST /users and POST /login) per client and window; 0 disables it.
+ auth_rate_limit: int = 30
+ auth_rate_window: int = 60
+ # When set, signup requires this invite code (form field ``invite`` or the
+ # ``X-Signup-Invite`` header). None keeps signup open, as in the demo.
+ signup_invite_code: str | None = None
+ # Optional allowlist of client request headers to forward upstream. None
+ # keeps the default denylist behavior; a set (possibly empty) forwards only
+ # the named headers (plus the gateway's own X-Forwarded-* values).
+ upstream_header_allowlist: frozenset[str] | None = None
+ # Concurrent proxied requests per user before 503/1011 is returned.
+ max_in_flight_per_user: int = 32
+ # Largest WebSocket message accepted from DSH, in bytes. Browser-to-DSH
+ # frames are bounded by the ASGI server's own WebSocket limit.
+ ws_max_message_bytes: int = 16 * 1024 * 1024
@classmethod
def from_env(cls) -> Settings:
+ cookie_secure = _env_bool("COOKIE_SECURE", False)
+ allow_insecure = _env_bool("ALLOW_INSECURE_COOKIES", False)
+ if not cookie_secure and not allow_insecure:
+ raise RuntimeError(
+ "refusing to start with an insecure session cookie: set "
+ "COOKIE_SECURE=true behind TLS, or ALLOW_INSECURE_COOKIES=1 "
+ "for a local plain-HTTP demo"
+ )
+ cookie_name = os.environ.get("COOKIE_NAME") or (
+ SECURE_COOKIE_NAME if cookie_secure else DEFAULT_COOKIE_NAME
+ )
return cls(
namespace=os.environ.get("NAMESPACE", "dsh-demo"),
pool=os.environ.get("POOL", "dsh-pool"),
@@ -350,13 +429,19 @@ class Settings:
dsh_port=_env_int("DSH_PORT", 3080),
dsh_container=os.environ.get("DSH_CONTAINER", "dsh-web"),
dsh_log_tail_lines=_env_int("DSH_LOG_TAIL_LINES", 2000),
- cookie_name=os.environ.get("COOKIE_NAME", "dsh_gw_session"),
- cookie_secure=_env_bool("COOKIE_SECURE", False),
+ cookie_name=cookie_name,
+ cookie_secure=cookie_secure,
cookie_max_age=_env_int("COOKIE_MAX_AGE", 7 * 24 * 60 * 60),
max_body_bytes=_env_int("MAX_BODY_MB", 32) * 1024 * 1024,
http_timeout=_env_float("HTTP_TIMEOUT", 300.0),
sweep_interval=_env_float("SWEEP_INTERVAL", 15.0),
auth_proxy_header=os.environ.get("AUTH_PROXY_HEADER") or None,
+ auth_rate_limit=_env_int("AUTH_RATE_LIMIT", 30),
+ auth_rate_window=_env_int("AUTH_RATE_WINDOW", 60),
+ signup_invite_code=os.environ.get("SIGNUP_INVITE_CODE") or None,
+ upstream_header_allowlist=_env_allowlist("UPSTREAM_HEADER_ALLOWLIST"),
+ max_in_flight_per_user=_env_int("MAX_IN_FLIGHT_PER_USER", 32),
+ ws_max_message_bytes=_env_int("WS_MAX_MESSAGE_BYTES", 16 * 1024 * 1024),
)
@@ -546,6 +631,19 @@ class Kubernetes:
# --- Gateway ---------------------------------------------------------------
+@dataclass(frozen=True)
+class Session:
+ """A browser session stored server-side, keyed by an opaque random id.
+
+ The cookie only carries the id; the user and the expiry stay in memory, so
+ a cookie can never be edited into another user's credentials and a stolen
+ token is not recoverable from the browser.
+ """
+
+ user: str
+ expires_at: float
+
+
@dataclass
class GatewayState:
"""In-memory, per-process state (single replica by design)."""
@@ -555,6 +653,10 @@ class GatewayState:
maintenance: dict[str, bool] = field(default_factory=dict)
# (user, pod UID, browser Host) -> "dsh-auth-a=...; dsh-auth-b=..."
handshake: dict[tuple[str, str, str], str] = field(default_factory=dict)
+ # Opaque session id -> server-side session (user + expiry).
+ sessions: dict[str, Session] = field(default_factory=dict)
+ # Auth rate limiting: client -> (window start, attempts in window).
+ auth_rate: dict[str, tuple[float, int]] = field(default_factory=dict)
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
@@ -576,17 +678,40 @@ class Gateway:
# -- credentials -------------------------------------------------------
+ def session_for(self, sid: str | None) -> Session | None:
+ """Look up a live session, dropping it when it has expired."""
+ if not sid:
+ return None
+ session = self.state.sessions.get(sid)
+ if session is None:
+ return None
+ if session.expires_at <= time.time():
+ self.state.sessions.pop(sid, None)
+ return None
+ return session
+
+ def revoke_session(self, sid: str | None) -> None:
+ if sid:
+ self.state.sessions.pop(sid, None)
+
+ def revoke_sessions(self, user: str) -> None:
+ for sid in [key for key, session in self.state.sessions.items() if session.user == user]:
+ self.state.sessions.pop(sid, None)
+
def credentials(self, headers, cookies) -> tuple[str | None, str | None]:
- """Return ``(token, cookie_user)``; bearer wins over the session cookie."""
+ """Return ``(token, session_user)``; bearer wins over the session cookie.
+
+ A bearer token is never resolved from a cookie and a session id is never
+ accepted as a bearer token. An unknown or expired session is treated as
+ anonymous (and the expired entry is dropped).
+ """
auth = headers.get("authorization", "")
if auth.lower().startswith("bearer "):
return auth[7:].strip(), None
- raw = cookies.get(self.settings.cookie_name)
- if raw:
- user, sep, token = raw.partition(":")
- if sep and user and token:
- return token, user
- return None, None
+ session = self.session_for(cookies.get(self.settings.cookie_name))
+ if session is None:
+ return None, None
+ return None, session.user
def remote_user(self, headers) -> str | None:
"""The user asserted by a trusted reverse proxy, when SSO is enabled.
@@ -605,31 +730,37 @@ class Gateway:
self,
claim: dict,
token: str | None,
- cookie_user: str | None,
+ session_user: str | None,
user: str,
remote: str | None = None,
) -> bool:
if remote == user:
return True # identity asserted by the authenticating proxy
- if cookie_user is not None and cookie_user != user:
- return False # a session for another user never authorizes this one
+ if session_user is not None:
+ return session_user == user # server-side session bound to this user
return token_matches(claim, token)
async def _sso_session(self, remote: str, headers, cookies, response: Response) -> dict | None:
- """Provision ``remote`` idempotently and mint/refresh a session token.
+ """Provision ``remote`` idempotently and mint/refresh a session.
The authenticating proxy has already verified ``remote``, so it is
trusted. A brand-new user is provisioned (claim created, token minted);
- an existing user whose session cookie is missing or no longer matches
- gets a rotated token so a fresh cookie can be issued. A still-valid
- session is reused untouched. Provisioning is non-blocking: the sandbox
- is awaited by the proxy path (``_resolve_sandbox``), so the landing page
- can report "Provisioning" instead of stalling for ``WAKE_TIMEOUT``.
+ an existing user whose session cookie is missing, foreign or no longer
+ matches gets a rotated token so a fresh cookie can be issued. A still-valid
+ session for the same user is reused untouched. Provisioning is
+ non-blocking: the sandbox is awaited by the proxy path
+ (``_resolve_sandbox``), so the landing page can report "Provisioning"
+ instead of stalling for ``WAKE_TIMEOUT``.
"""
claim = await self.k8s.get_claim(remote)
- token, cookie_user = self.credentials(headers, cookies)
- if claim is not None and cookie_user == remote and token_matches(claim, token):
- return claim
+ sid = cookies.get(self.settings.cookie_name)
+ session = self.session_for(sid)
+ if session is not None:
+ if session.user != remote:
+ # A session for another identity never authorizes this user.
+ self.revoke_session(sid)
+ elif claim is not None:
+ return claim
token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(token.encode()).hexdigest()
is_new = claim is None
@@ -645,7 +776,7 @@ class Gateway:
else:
await self.k8s.patch_token(remote, token_hash)
await self._touch(remote)
- self.set_session(response, remote, token)
+ self.set_session(response, remote)
return await self.k8s.get_claim(remote) if is_new else claim
@staticmethod
@@ -656,16 +787,73 @@ class Gateway:
and not same_origin(headers)
)
- def set_session(self, response: Response, user: str, token: str) -> None:
+ def set_session(self, response: Response, user: str) -> str:
+ """Mint an opaque server-side session and set the browser cookie."""
+ sid = secrets.token_urlsafe(32)
+ self.state.sessions[sid] = Session(
+ user=user,
+ expires_at=time.time() + self.settings.cookie_max_age,
+ )
response.set_cookie(
self.settings.cookie_name,
- f"{user}:{token}",
+ sid,
max_age=self.settings.cookie_max_age,
path="/",
httponly=True,
samesite="lax",
secure=self.settings.cookie_secure,
)
+ return sid
+
+ # -- abuse controls ----------------------------------------------------
+
+ def _rate_limit(self, request: Request) -> Response | None:
+ """Return a 429 response when this client is over the auth budget."""
+ limit = self.settings.auth_rate_limit
+ if limit <= 0:
+ return None
+ window = max(1, self.settings.auth_rate_window)
+ key = client_identity(request.headers, request.client)
+ now = time.time()
+ bucket = self.state.auth_rate.get(key)
+ if bucket is None or now - bucket[0] >= window:
+ self.state.auth_rate[key] = (now, 1)
+ return None
+ start, count = bucket
+ if count >= limit:
+ retry_after = max(1, math.ceil(start + window - now))
+ return self._error(
+ 429,
+ "too many authentication attempts, retry shortly",
+ {"Retry-After": str(retry_after)},
+ )
+ self.state.auth_rate[key] = (start, count + 1)
+ return None
+
+ def _invite_allowed(self, request: Request, payload: dict) -> bool:
+ """Check the signup invite code, when one is configured."""
+ expected = self.settings.signup_invite_code
+ if expected is None:
+ return True
+ provided = payload.get("invite")
+ if not isinstance(provided, str) or not provided:
+ provided = request.headers.get("x-signup-invite", "")
+ if not isinstance(provided, str) or not provided:
+ return False
+ return secrets.compare_digest(provided.encode("utf-8"), expected.encode("utf-8"))
+
+ def _form_cross_origin(self, request: Request) -> bool:
+ """Reject a form signup/login whose Origin/Referer contradicts Host.
+
+ Only checked when the browser actually sent one: non-browser clients
+ (automation, tests) commonly omit both headers and must keep working.
+ """
+ if not self._wants_form(request):
+ return False
+ headers = request.headers
+ if not (headers.get("origin") or headers.get("referer")):
+ return False
+ return not same_origin(headers)
# -- activity / maintenance -------------------------------------------
@@ -674,12 +862,21 @@ class Gateway:
self.state.last_activity[user] = time.time()
async def _acquire(self, user: str) -> bool:
- """Reserve an in-flight slot, waiting out a concurrent suspend."""
+ """Reserve an in-flight slot, waiting out a concurrent suspend.
+
+ Returns False when the user already has ``MAX_IN_FLIGHT_PER_USER``
+ requests in flight (an abuse guard, not a transient error) or when a
+ suspend does not finish within ``WAKE_TIMEOUT``.
+ """
deadline = time.monotonic() + self.settings.wake_timeout
+ cap = self.settings.max_in_flight_per_user
while True:
async with self.state.lock:
if not self.state.maintenance.get(user):
- self.state.in_flight[user] = self.state.in_flight.get(user, 0) + 1
+ current = self.state.in_flight.get(user, 0)
+ if cap > 0 and current >= cap:
+ return False
+ self.state.in_flight[user] = current + 1
self.state.last_activity[user] = time.time()
return True
if time.monotonic() > deadline:
@@ -759,14 +956,19 @@ class Gateway:
host: str,
force: bool = False,
) -> tuple[str, tuple[str, str, str], bool]:
- """Return ``(cookie, cache_key, was_cached)``, handshaking on a miss."""
+ """Return ``(cookie, cache_key, was_cached)``, handshaking on a miss.
+
+ Every failure raises ``HandshakeError`` with a fixed, credential-free
+ message: the exception reaches the browser in the 503 body, so it must
+ never carry the launch token, the handshake URL or pod details.
+ """
pod_name = self._pod_name(sandbox)
try:
pod = await self.k8s.get_pod(pod_name)
except ApiException as exc:
- raise HandshakeError(f"could not read pod {pod_name}: {exc}") from exc
+ raise HandshakeError("could not read the DSH handshake pod") from exc
if pod is None:
- raise HandshakeError(f"pod {pod_name} not found")
+ raise HandshakeError("the DSH handshake pod is not available")
uid = (pod.get("metadata") or {}).get("uid") or pod_name
key = (user, uid, host)
if not force:
@@ -780,7 +982,7 @@ class Gateway:
self.settings.dsh_log_tail_lines,
)
except ApiException as exc:
- raise HandshakeError(f"could not read logs for {pod_name}: {exc}") from exc
+ raise HandshakeError("could not read the pod logs for the handshake") from exc
launch_token = parse_dsh_launch_token(logs)
if not launch_token:
raise HandshakeError("DSH launch token not found in pod logs")
@@ -798,12 +1000,12 @@ class Gateway:
timeout=self.settings.http_timeout,
)
except httpx.HTTPError as exc:
- raise HandshakeError(f"DSH handshake failed: {exc}") from exc
+ raise HandshakeError("the DSH handshake request failed") from exc
if response.status_code >= 400:
- raise HandshakeError(f"DSH handshake returned {response.status_code}")
+ raise HandshakeError("the DSH handshake returned an error status")
cookie = extract_dsh_cookies(response.headers.get_list("set-cookie"))
if not cookie:
- raise HandshakeError("DSH handshake set no dsh-auth cookie")
+ raise HandshakeError("the DSH handshake set no auth cookie")
self.state.handshake[key] = cookie
return cookie, key, False
@@ -879,9 +1081,9 @@ class Gateway:
if "set-cookie" in response.headers:
page.headers["set-cookie"] = response.headers["set-cookie"]
return page
- token, user = self.credentials(request.headers, request.cookies)
+ _token, user = self.credentials(request.headers, request.cookies)
session = ""
- if user and token:
+ if user:
safe_user = html.escape(user)
session = (
f'
Signed in as {safe_user}. '
@@ -889,6 +1091,11 @@ class Gateway:
'
'
)
+ invite_input = (
+ ''
+ if self.settings.signup_invite_code is not None
+ else ""
+ )
body = f"""
@@ -911,6 +1118,7 @@ class Gateway:
Create a sandbox
@@ -970,11 +1178,18 @@ against the JSON API. The browser session is an HttpOnly cookie.
# -- users -------------------------------------------------------------
async def signup(self, request: Request) -> Response:
+ limited = self._rate_limit(request)
+ if limited is not None:
+ return limited
+ if self._form_cross_origin(request):
+ return self._error(403, "cross-origin request rejected")
wants_form = self._wants_form(request)
try:
payload = await self._read_payload(request)
except BodyTooLarge:
return self._error(413, "request body too large")
+ if not self._invite_allowed(request, payload):
+ return self._error(403, "invalid or missing invite code")
user = payload.get("user", "")
if not valid_user(user):
return self._error(400, "body must be {'user': ''}")
@@ -992,16 +1207,21 @@ against the JSON API. The browser session is an HttpOnly cookie.
sandbox_name = (sandbox or {}).get("metadata", {}).get("name")
if wants_form:
response = RedirectResponse(f"/u/{user}/", status_code=303)
- self.set_session(response, user, token)
+ self.set_session(response, user)
return response
response = JSONResponse(
{"user": user, "token": token, "state": state, "sandbox": sandbox_name},
status_code=201 if state == "Ready" else 202,
)
- self.set_session(response, user, token)
+ self.set_session(response, user)
return response
async def login(self, request: Request) -> Response:
+ limited = self._rate_limit(request)
+ if limited is not None:
+ return limited
+ if self._form_cross_origin(request):
+ return self._error(403, "cross-origin request rejected")
wants_form = self._wants_form(request)
try:
payload = await self._read_payload(request)
@@ -1012,21 +1232,29 @@ against the JSON API. The browser session is an HttpOnly cookie.
if not isinstance(user, str) or not isinstance(token, str):
return self._error(400, "user and token are required")
claim = await self.k8s.get_claim(user)
- if claim is None:
- return self._error(404, "not found")
- if not token_matches(claim, token):
+ # An unknown user is indistinguishable from a bad token to the client:
+ # never reveal which usernames exist.
+ if claim is None or not token_matches(claim, token):
return self._error(401, "unauthorized")
await self._touch(user)
if wants_form:
response = RedirectResponse(f"/u/{user}/", status_code=303)
else:
response = JSONResponse({"user": user, "state": "ok"})
- self.set_session(response, user, token)
+ self.set_session(response, user)
return response
async def logout(self, request: Request) -> Response:
+ self.revoke_session(request.cookies.get(self.settings.cookie_name))
response = RedirectResponse("/", status_code=303)
- response.delete_cookie(self.settings.cookie_name, path="/")
+ # Match the attributes the cookie was set with: a __Host- cookie
+ # deletion that lacks Secure is rejected by browsers, leaving a stale
+ # (though already revoked) cookie behind.
+ response.delete_cookie(
+ self.settings.cookie_name,
+ path="/",
+ secure=self.settings.cookie_secure,
+ )
return response
async def get_user(self, request: Request) -> Response:
@@ -1061,6 +1289,7 @@ against the JSON API. The browser session is an HttpOnly cookie.
if self._mutation_from_cookie(request.headers, "DELETE", cookie_user):
return self._error(403, "cross-origin request rejected")
await self.k8s.delete_claim(user)
+ self.revoke_sessions(user)
self._clear_handshake(user)
async with self.state.lock:
self.state.last_activity.pop(user, None)
@@ -1084,6 +1313,7 @@ against the JSON API. The browser session is an HttpOnly cookie.
if self._mutation_from_cookie(request.headers, "POST", cookie_user):
return self._error(403, "cross-origin request rejected")
await self.k8s.delete_claim(remote)
+ self.revoke_sessions(remote)
self._clear_handshake(remote)
async with self.state.lock:
self.state.last_activity.pop(remote, None)
@@ -1111,7 +1341,7 @@ against the JSON API. The browser session is an HttpOnly cookie.
return RedirectResponse("/", status_code=303)
return self._error(404, "unknown user")
token, cookie_user = self.credentials(request.headers, request.cookies)
- if remote == user and not (cookie_user == user and token_matches(claim, token)):
+ if remote == user and cookie_user != user:
# SSO user without a valid session cookie: bootstrap, then continue.
return RedirectResponse("/", status_code=303)
if not self._authorized(claim, token, cookie_user, user, remote):
@@ -1171,11 +1401,17 @@ against the JSON API. The browser session is an HttpOnly cookie.
if query:
url = f"{url}?{query}"
- headers = {
- key: value
- for key, value in request.headers.items()
- if key.lower() not in OWNED_REQUEST_HEADERS and key.lower() != "content-length"
- }
+ headers = {}
+ allowlist = self.settings.upstream_header_allowlist
+ for key, value in request.headers.items():
+ lowered = key.lower()
+ if lowered in OWNED_REQUEST_HEADERS or lowered == "content-length":
+ continue
+ if strip_forwarding_header(lowered):
+ continue # client-supplied identity/hop-chain headers are spoofable
+ if allowlist is not None and lowered not in allowlist:
+ continue
+ headers[key] = value
host = request.headers.get("host", "")
headers["host"] = host
headers["cookie"] = cookie # only the cached DSH auth cookie
@@ -1309,13 +1545,19 @@ against the JSON API. The browser session is an HttpOnly cookie.
query = filtered_query(websocket.url.query)
if query:
url = f"{url}?{query}"
- headers = {
- key: value
- for key, value in websocket.headers.items()
- if key.lower() not in OWNED_REQUEST_HEADERS
- and key.lower() != "origin"
- and not key.lower().startswith("sec-websocket-")
- }
+ headers = {}
+ allowlist = self.settings.upstream_header_allowlist
+ for key, value in websocket.headers.items():
+ lowered = key.lower()
+ if lowered in OWNED_REQUEST_HEADERS or lowered == "origin":
+ continue
+ if lowered.startswith("sec-websocket-"):
+ continue
+ if strip_forwarding_header(lowered):
+ continue # client-supplied identity/hop-chain headers
+ if allowlist is not None and lowered not in allowlist:
+ continue
+ headers[key] = value
headers["cookie"] = cookie
headers["x-forwarded-host"] = host
headers["x-forwarded-proto"] = websocket.url.scheme
@@ -1331,7 +1573,7 @@ against the JSON API. The browser session is an HttpOnly cookie.
additional_headers=headers,
subprotocols=subprotocols or None,
open_timeout=self.settings.wake_timeout,
- max_size=None,
+ max_size=self.settings.ws_max_message_bytes,
proxy=None,
)
upstream = await upstream_cm.__aenter__()
@@ -1391,8 +1633,41 @@ against the JSON API. The browser session is an HttpOnly cookie.
# -- idle sweeper ------------------------------------------------------
+ def _prune_state(self, claims: list[dict]) -> None:
+ """Drop state that can no longer be reached.
+
+ Sessions and activity clocks for a user whose claim is gone, expired
+ sessions and expired rate-limit buckets would otherwise accumulate for
+ the lifetime of the process. In-flight counters are only pruned at
+ zero, and a user with work in flight is always preserved.
+ """
+ now = time.time()
+ present = set()
+ for claim in claims:
+ name = (claim.get("metadata") or {}).get("name") or ""
+ if name.startswith("dsh-"):
+ present.add(name[len("dsh-") :])
+ for user in [user for user in self.state.last_activity if user not in present]:
+ self.state.last_activity.pop(user, None)
+ for user, count in list(self.state.in_flight.items()):
+ if user not in present and count == 0:
+ self.state.in_flight.pop(user, None)
+ for user, flag in list(self.state.maintenance.items()):
+ if user not in present and not flag:
+ self.state.maintenance.pop(user, None)
+ for key in [key for key in self.state.handshake if key[0] not in present]:
+ self.state.handshake.pop(key, None)
+ for sid, session in list(self.state.sessions.items()):
+ if session.user not in present or session.expires_at <= now:
+ self.state.sessions.pop(sid, None)
+ window = max(1, self.settings.auth_rate_window)
+ for key, (start, _count) in list(self.state.auth_rate.items()):
+ if now - start >= window:
+ self.state.auth_rate.pop(key, None)
+
async def sweep_once(self) -> None:
claims = await self.k8s.list_claims()
+ self._prune_state(claims)
for claim in claims:
# Only suspend claims this gateway created. Everything else in
# the namespace (resource-layer claims from the README walkthrough,
diff --git a/examples/dsh-agents-as-a-service/gateway/requirements-dev.txt b/examples/dsh-agents-as-a-service/gateway/requirements-dev.txt
index eef29c9..e47ffd4 100644
--- a/examples/dsh-agents-as-a-service/gateway/requirements-dev.txt
+++ b/examples/dsh-agents-as-a-service/gateway/requirements-dev.txt
@@ -1,5 +1,1740 @@
--r requirements.txt
-pytest==9.1.1
-pytest-asyncio==1.4.0
-pytest-cov==7.1.0
-ruff==0.17.2
+# Hash-pinned development dependencies for the example gateway: runtime
+# dependencies plus the test/lint tooling CI uses (pytest, pytest-asyncio,
+# pytest-cov, ruff). Fully resolved, so it is installed on its own.
+#
+# Generated file -- do not hand-edit. Both this file and requirements.txt are
+# installed with "pip install --require-hashes" in CI. To change a dependency,
+# edit the pin below and regenerate with:
+#
+# UV_CACHE_DIR=$PWD/.uvcache uv pip compile requirements-dev.txt \
+# --generate-hashes --python-version 3.12 --no-annotate --no-header \
+# -o requirements-dev.txt
+aiohappyeyeballs==2.7.1 \
+ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \
+ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472
+aiohttp==3.14.5 \
+ --hash=sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1 \
+ --hash=sha256:038c2c7e8caa26b6c8423779b5eaf1893904048a512c19b32fe841ffa5592b50 \
+ --hash=sha256:0684952aeae1f5dbfe02d46039338513b94009baecd15d8e4098a357c4c4a2a6 \
+ --hash=sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f \
+ --hash=sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d \
+ --hash=sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210 \
+ --hash=sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a \
+ --hash=sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f \
+ --hash=sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb \
+ --hash=sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e \
+ --hash=sha256:1220353657ad49493551f089ce02f1a348fd57ffd585bfec77f2f3c4fe3a7346 \
+ --hash=sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1 \
+ --hash=sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9 \
+ --hash=sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51 \
+ --hash=sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155 \
+ --hash=sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863 \
+ --hash=sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd \
+ --hash=sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30 \
+ --hash=sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b \
+ --hash=sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738 \
+ --hash=sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec \
+ --hash=sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7 \
+ --hash=sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382 \
+ --hash=sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6 \
+ --hash=sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f \
+ --hash=sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf \
+ --hash=sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584 \
+ --hash=sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20 \
+ --hash=sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12 \
+ --hash=sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789 \
+ --hash=sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658 \
+ --hash=sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c \
+ --hash=sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781 \
+ --hash=sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2 \
+ --hash=sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111 \
+ --hash=sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57 \
+ --hash=sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4 \
+ --hash=sha256:330900acd0dc4cb8b27f9c127fbaad770964845338493e7906ae3822e82dbf8d \
+ --hash=sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab \
+ --hash=sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c \
+ --hash=sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748 \
+ --hash=sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d \
+ --hash=sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e \
+ --hash=sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0 \
+ --hash=sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79 \
+ --hash=sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae \
+ --hash=sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3 \
+ --hash=sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7 \
+ --hash=sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421 \
+ --hash=sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793 \
+ --hash=sha256:4f07fe3ac408d8b3f768be471dc3f56d43843c47d97c66120534467a15ead197 \
+ --hash=sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0 \
+ --hash=sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016 \
+ --hash=sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045 \
+ --hash=sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178 \
+ --hash=sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622 \
+ --hash=sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480 \
+ --hash=sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f \
+ --hash=sha256:5af42135fdfebdadbc2bcd9c0842a48ccf0d62794c36a260b21dc4b94d1e0119 \
+ --hash=sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3 \
+ --hash=sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29 \
+ --hash=sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939 \
+ --hash=sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3 \
+ --hash=sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c \
+ --hash=sha256:602c1e9b718a3275c580149f947e7fac65044c0a20e599553fb12e9700da9eca \
+ --hash=sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef \
+ --hash=sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d \
+ --hash=sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343 \
+ --hash=sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d \
+ --hash=sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905 \
+ --hash=sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc \
+ --hash=sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67 \
+ --hash=sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0 \
+ --hash=sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794 \
+ --hash=sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835 \
+ --hash=sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d \
+ --hash=sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d \
+ --hash=sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c \
+ --hash=sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e \
+ --hash=sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da \
+ --hash=sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9 \
+ --hash=sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115 \
+ --hash=sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c \
+ --hash=sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84 \
+ --hash=sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc \
+ --hash=sha256:8df7d481654ac96fe1ba9a02a9f67770fdd367823e0d5ef01b922725c4bd2cfa \
+ --hash=sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f \
+ --hash=sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4 \
+ --hash=sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5 \
+ --hash=sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c \
+ --hash=sha256:96a2e584f0b9ed8f1fa33211397dcf67bb7069866402cb405d191c2f0defb9a3 \
+ --hash=sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a \
+ --hash=sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14 \
+ --hash=sha256:9bab2045550c4fe0f7baf89574db1b455c195750702ba96fef1f16972b146617 \
+ --hash=sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4 \
+ --hash=sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d \
+ --hash=sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2 \
+ --hash=sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820 \
+ --hash=sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed \
+ --hash=sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd \
+ --hash=sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593 \
+ --hash=sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac \
+ --hash=sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833 \
+ --hash=sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee \
+ --hash=sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3 \
+ --hash=sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600 \
+ --hash=sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d \
+ --hash=sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703 \
+ --hash=sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764 \
+ --hash=sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99 \
+ --hash=sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a \
+ --hash=sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755 \
+ --hash=sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84 \
+ --hash=sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5 \
+ --hash=sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3 \
+ --hash=sha256:b7806e804889231b0e06469fd4a5c06313d1c0a3377322b6d9237fa5e0fe4167 \
+ --hash=sha256:bea559ad70218d230663e4210875735076a9bfea5994cef34a55a25faeaf2544 \
+ --hash=sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1 \
+ --hash=sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946 \
+ --hash=sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e \
+ --hash=sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4 \
+ --hash=sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02 \
+ --hash=sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba \
+ --hash=sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02 \
+ --hash=sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3 \
+ --hash=sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a \
+ --hash=sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412 \
+ --hash=sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e \
+ --hash=sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec \
+ --hash=sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319 \
+ --hash=sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2 \
+ --hash=sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11 \
+ --hash=sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073 \
+ --hash=sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f \
+ --hash=sha256:d94e44be379e569758fee8a9a58431cfc3c2598c708b92b1cfe96c66b4c94aef \
+ --hash=sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603 \
+ --hash=sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2 \
+ --hash=sha256:dca3fa8d8a0a26679862eccb0b1a9151b2b9f1cd2c212e7a6335778faaff5833 \
+ --hash=sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450 \
+ --hash=sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a \
+ --hash=sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a \
+ --hash=sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59 \
+ --hash=sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db \
+ --hash=sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329 \
+ --hash=sha256:e95c8def4b81c5d68d5cf1f54c07acd7c0d2577af244e5b6da802120825737c6 \
+ --hash=sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1 \
+ --hash=sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8 \
+ --hash=sha256:ef692a24087a699c0a4a26af45e746e0c1eae2116f6d8a5ff91d8aae2b867b45 \
+ --hash=sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b \
+ --hash=sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09 \
+ --hash=sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e \
+ --hash=sha256:f2ebb54b3f932210503072f09974b4fb574d823e497a944adfdcd140a6a00255 \
+ --hash=sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1 \
+ --hash=sha256:f375db73a39f5cf83696d500e21a67f418dc9a988955756f254be8f03b7b3651 \
+ --hash=sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91 \
+ --hash=sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8 \
+ --hash=sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d \
+ --hash=sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677 \
+ --hash=sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534 \
+ --hash=sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441 \
+ --hash=sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550
+aiohttp-retry==2.9.1 \
+ --hash=sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54 \
+ --hash=sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1
+aiosignal==1.4.0 \
+ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
+ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
+annotated-types==0.8.0 \
+ --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
+ --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
+anyio==4.15.1 \
+ --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \
+ --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94
+attrs==26.1.0 \
+ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
+ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
+certifi==2026.10.23 \
+ --hash=sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd \
+ --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
+charset-normalizer==3.5.2 \
+ --hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \
+ --hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \
+ --hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \
+ --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \
+ --hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \
+ --hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \
+ --hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \
+ --hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \
+ --hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \
+ --hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \
+ --hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \
+ --hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \
+ --hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \
+ --hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \
+ --hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \
+ --hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \
+ --hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \
+ --hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \
+ --hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \
+ --hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \
+ --hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \
+ --hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \
+ --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \
+ --hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \
+ --hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \
+ --hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \
+ --hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \
+ --hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \
+ --hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \
+ --hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \
+ --hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \
+ --hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \
+ --hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \
+ --hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \
+ --hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \
+ --hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \
+ --hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \
+ --hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \
+ --hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \
+ --hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \
+ --hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \
+ --hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \
+ --hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \
+ --hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \
+ --hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \
+ --hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \
+ --hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \
+ --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \
+ --hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \
+ --hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \
+ --hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \
+ --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \
+ --hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \
+ --hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \
+ --hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \
+ --hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \
+ --hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \
+ --hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \
+ --hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \
+ --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \
+ --hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \
+ --hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \
+ --hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \
+ --hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \
+ --hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \
+ --hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \
+ --hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \
+ --hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \
+ --hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \
+ --hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \
+ --hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \
+ --hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \
+ --hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \
+ --hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \
+ --hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \
+ --hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \
+ --hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \
+ --hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \
+ --hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \
+ --hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \
+ --hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \
+ --hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \
+ --hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \
+ --hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \
+ --hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \
+ --hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \
+ --hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \
+ --hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \
+ --hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \
+ --hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \
+ --hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \
+ --hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \
+ --hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \
+ --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \
+ --hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \
+ --hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \
+ --hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \
+ --hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \
+ --hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \
+ --hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \
+ --hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \
+ --hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \
+ --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \
+ --hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \
+ --hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \
+ --hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \
+ --hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \
+ --hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \
+ --hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \
+ --hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \
+ --hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \
+ --hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \
+ --hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \
+ --hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \
+ --hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \
+ --hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \
+ --hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \
+ --hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \
+ --hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \
+ --hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \
+ --hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \
+ --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \
+ --hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \
+ --hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \
+ --hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \
+ --hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \
+ --hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \
+ --hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \
+ --hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \
+ --hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \
+ --hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \
+ --hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \
+ --hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \
+ --hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \
+ --hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \
+ --hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \
+ --hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \
+ --hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \
+ --hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \
+ --hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \
+ --hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \
+ --hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \
+ --hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \
+ --hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \
+ --hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \
+ --hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \
+ --hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \
+ --hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \
+ --hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \
+ --hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \
+ --hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \
+ --hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \
+ --hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \
+ --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \
+ --hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \
+ --hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \
+ --hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \
+ --hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \
+ --hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \
+ --hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \
+ --hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \
+ --hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \
+ --hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \
+ --hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \
+ --hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \
+ --hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \
+ --hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \
+ --hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \
+ --hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \
+ --hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \
+ --hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \
+ --hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f
+click==8.5.0 \
+ --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
+ --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
+coverage==7.17.0 \
+ --hash=sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274 \
+ --hash=sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657 \
+ --hash=sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669 \
+ --hash=sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036 \
+ --hash=sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74 \
+ --hash=sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b \
+ --hash=sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51 \
+ --hash=sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca \
+ --hash=sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e \
+ --hash=sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95 \
+ --hash=sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3 \
+ --hash=sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6 \
+ --hash=sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6 \
+ --hash=sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2 \
+ --hash=sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03 \
+ --hash=sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8 \
+ --hash=sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438 \
+ --hash=sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea \
+ --hash=sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2 \
+ --hash=sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722 \
+ --hash=sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034 \
+ --hash=sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf \
+ --hash=sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33 \
+ --hash=sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c \
+ --hash=sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e \
+ --hash=sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709 \
+ --hash=sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7 \
+ --hash=sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2 \
+ --hash=sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35 \
+ --hash=sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c \
+ --hash=sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5 \
+ --hash=sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb \
+ --hash=sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7 \
+ --hash=sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968 \
+ --hash=sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa \
+ --hash=sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177 \
+ --hash=sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85 \
+ --hash=sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b \
+ --hash=sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673 \
+ --hash=sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802 \
+ --hash=sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241 \
+ --hash=sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28 \
+ --hash=sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398 \
+ --hash=sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4 \
+ --hash=sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f \
+ --hash=sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5 \
+ --hash=sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca \
+ --hash=sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b \
+ --hash=sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e \
+ --hash=sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9 \
+ --hash=sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36 \
+ --hash=sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764 \
+ --hash=sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e \
+ --hash=sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2 \
+ --hash=sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e \
+ --hash=sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e \
+ --hash=sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549 \
+ --hash=sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f \
+ --hash=sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2 \
+ --hash=sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569 \
+ --hash=sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983 \
+ --hash=sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69 \
+ --hash=sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094 \
+ --hash=sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5 \
+ --hash=sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e \
+ --hash=sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697 \
+ --hash=sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba \
+ --hash=sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062 \
+ --hash=sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3 \
+ --hash=sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67 \
+ --hash=sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c \
+ --hash=sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2 \
+ --hash=sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048 \
+ --hash=sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089 \
+ --hash=sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7 \
+ --hash=sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5 \
+ --hash=sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23 \
+ --hash=sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c \
+ --hash=sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592 \
+ --hash=sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a \
+ --hash=sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f \
+ --hash=sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559 \
+ --hash=sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1 \
+ --hash=sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c \
+ --hash=sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28 \
+ --hash=sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76 \
+ --hash=sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4 \
+ --hash=sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d \
+ --hash=sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95 \
+ --hash=sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d \
+ --hash=sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d \
+ --hash=sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429 \
+ --hash=sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c \
+ --hash=sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc \
+ --hash=sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88 \
+ --hash=sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e \
+ --hash=sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c \
+ --hash=sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350 \
+ --hash=sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109 \
+ --hash=sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712 \
+ --hash=sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3 \
+ --hash=sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a \
+ --hash=sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c \
+ --hash=sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40 \
+ --hash=sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5 \
+ --hash=sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56 \
+ --hash=sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a \
+ --hash=sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd \
+ --hash=sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f \
+ --hash=sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc \
+ --hash=sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83 \
+ --hash=sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e \
+ --hash=sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e \
+ --hash=sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398 \
+ --hash=sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d \
+ --hash=sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771 \
+ --hash=sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe \
+ --hash=sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f \
+ --hash=sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b
+durationpy==0.11 \
+ --hash=sha256:181898e1ae282e288f0a2291829656bf1b6b3aadf30a97993b85db4943642905 \
+ --hash=sha256:a739fe2b8972c250ff72f8e2c488d18cf25f7b852f49ee76048775d5171df30c
+frozenlist==1.8.0 \
+ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \
+ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \
+ --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \
+ --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \
+ --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \
+ --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \
+ --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \
+ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
+ --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \
+ --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \
+ --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \
+ --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \
+ --hash=sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4 \
+ --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \
+ --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \
+ --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \
+ --hash=sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3 \
+ --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \
+ --hash=sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087 \
+ --hash=sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068 \
+ --hash=sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7 \
+ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \
+ --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \
+ --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \
+ --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \
+ --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \
+ --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \
+ --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \
+ --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \
+ --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \
+ --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \
+ --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \
+ --hash=sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675 \
+ --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \
+ --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \
+ --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \
+ --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \
+ --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \
+ --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \
+ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
+ --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \
+ --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \
+ --hash=sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c \
+ --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \
+ --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \
+ --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \
+ --hash=sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5 \
+ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \
+ --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \
+ --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \
+ --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \
+ --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \
+ --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \
+ --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \
+ --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \
+ --hash=sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95 \
+ --hash=sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1 \
+ --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \
+ --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \
+ --hash=sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6 \
+ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \
+ --hash=sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459 \
+ --hash=sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a \
+ --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \
+ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \
+ --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \
+ --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \
+ --hash=sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186 \
+ --hash=sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6 \
+ --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \
+ --hash=sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e \
+ --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \
+ --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \
+ --hash=sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450 \
+ --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \
+ --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \
+ --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \
+ --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \
+ --hash=sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178 \
+ --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \
+ --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \
+ --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \
+ --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \
+ --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \
+ --hash=sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61 \
+ --hash=sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca \
+ --hash=sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad \
+ --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \
+ --hash=sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a \
+ --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \
+ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \
+ --hash=sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011 \
+ --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \
+ --hash=sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103 \
+ --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \
+ --hash=sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda \
+ --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \
+ --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \
+ --hash=sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e \
+ --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \
+ --hash=sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef \
+ --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \
+ --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \
+ --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \
+ --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \
+ --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \
+ --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \
+ --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \
+ --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \
+ --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \
+ --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \
+ --hash=sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47 \
+ --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \
+ --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \
+ --hash=sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f \
+ --hash=sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff \
+ --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \
+ --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \
+ --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \
+ --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \
+ --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \
+ --hash=sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 \
+ --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \
+ --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \
+ --hash=sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 \
+ --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \
+ --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \
+ --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \
+ --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \
+ --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd
+h11==0.16.0 \
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
+httpcore==1.0.9 \
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
+httptools==0.9.0 \
+ --hash=sha256:02bc5b3dcb6394b9d825fd62a7bfa0b2943063a3c89abc4492ad45e334a20eb5 \
+ --hash=sha256:050f7ab098121873c8f13e35857f97ab60a76185c8302bde9a384939bb7c3b96 \
+ --hash=sha256:050f84b7ec46a6efe0e5f521cf8729e3397c1cef4384f62ed8d5d68ca0045776 \
+ --hash=sha256:06bfe7fad972a417269d8a5fc53b87e4eca970354abf5e9e24336fd06d64292e \
+ --hash=sha256:088de1738e1af624466a01c35d652dbe6fb825be887c76d68aa850621d81db88 \
+ --hash=sha256:0adc974916efe1fbf89d0363a86dcb2c746727643e362ff398de1a4b50b6bc77 \
+ --hash=sha256:0cc339a807c156d840b54f8bf050ba0fc265eb81692c24bca8535b52fbd797c6 \
+ --hash=sha256:0fd73d0bbf700a30dd87e4412adf41cfa71542a533d6b390c7244bbb8a1152bb \
+ --hash=sha256:130635fea6e611a6b2026120037965ddb88b3dafd11bb64e264b101a70a76630 \
+ --hash=sha256:13873eb8aef5972fcfee614f63d47064312ad4efbfe65ade15b8a3b77f8c8659 \
+ --hash=sha256:18d800aaa2d6bff7d889df810d1b19a5fde72b1f6c0ca96e8d9f28a692fe5460 \
+ --hash=sha256:1a4050a651e1f2faf05eb028ce9f2168abbcee9e24b209f5c1f2eb96d8c569e4 \
+ --hash=sha256:1a7f1df31829c258158be01bb04eb668c4fba7df1ddf2262131a972962e651b6 \
+ --hash=sha256:1b01c0fcd6725a8d79a164ecdc4116866282479d68bb3d6d74a909bf994656c4 \
+ --hash=sha256:1b95775f6292d72cb452c33e5c0f8b8551807c29a10e3c1671fef7f61361370a \
+ --hash=sha256:1f6da814aeecbc6cb8872d6d3e85ed16e8ab1653f9557cea8658725ce212348a \
+ --hash=sha256:2095207b75a83c9e947346da9c127fb7e4fb29f41589df2643764f06b750989c \
+ --hash=sha256:22ab1b10b06d357f01092e60f5e6856a0d479ed79b0ec2166a339ea26c699be2 \
+ --hash=sha256:2319858018eedd0c0b2f950a620413c0a9d1352607be4267eb28209eca8b1e3f \
+ --hash=sha256:268d18601feb5367885c6ebf6f402c18fc25a324cee215784adafe0a1eef925f \
+ --hash=sha256:26e1d9629f3bf70d23f0d22238152aec51c837a7c9e384cb74f356fdccad7eb3 \
+ --hash=sha256:272db0c51e8b71e953c1f2ecbe63402b819680e4564be2ef285cfd4584ee8355 \
+ --hash=sha256:289f213d2a3dde2e8312c415ffecec5a01698589ec6249ec4e8fb3b47c0444ba \
+ --hash=sha256:29b0d823e3c1e7cd1093a5dc889245db693ef13ada624cd66e2262421ef38867 \
+ --hash=sha256:310266a2db1377ffae3bdf6556ab4973f4f94508a8ce37b2f6bb096a89bcefa1 \
+ --hash=sha256:3238e198429cb8909ec42951b82d6a33fe0fdfcf86371732f8f09311c5b8ac32 \
+ --hash=sha256:34266cec8c1d4e3e91fcca7efe38971d6bdda64a7944f2a46ab576da15173680 \
+ --hash=sha256:36fac804b8cfd6b935ae64f71349f833d2b6298404626d017a2c57bb942bc643 \
+ --hash=sha256:3af4e45ff455fce5511fdf2653c1ce428ef09c56fe37a83eb4d924c2d474f31e \
+ --hash=sha256:3e3201fe4d46e0d15d7ff9fafc94a605da9eb82d2c5b9837f0368acb325481f1 \
+ --hash=sha256:45b3002392948dcf578029c89f6318e1289a993a1a5ec38a4161560fab60f811 \
+ --hash=sha256:465bc1526debf53a3be92022a16ca0c38f891ea3b5c1587af4f52e44020f8a07 \
+ --hash=sha256:48c705bd0b1afb6253ed71eca9f9ba7ac7d47838e5fed1ef7891d67f21ecd4de \
+ --hash=sha256:4a4d8c2c7e73ba5967be74d7c3a5ff81fde815ee1b48d9c5c0f14de8463a847b \
+ --hash=sha256:4a85401b0c3f893cf5695c1199e8679fbf673f7f78c2f6c11d6b1850f8c7e358 \
+ --hash=sha256:4c58dc91aefb31adad500aa68054334f429b840b36dd29e34e834101044cb2ef \
+ --hash=sha256:4efbee349138a3fee7a4cc3a95abd2d499fae70dd5bff9fed9138d6f570f4283 \
+ --hash=sha256:4fb995082fe41ec410b33c48b54fb1d44abb8a6ee762c31e8c42519e8c3a30a9 \
+ --hash=sha256:5042aa1c7e2b1a24c17dab31d8770b63a5101c9abc25f832c6aef6b201e1ca4f \
+ --hash=sha256:52fe0176682a25b15370f23f5b0f1366a84771df89144fb0cd979cb72a94b5ca \
+ --hash=sha256:5332a020a60bbe32ede4bda1a62b3d56c4831d309cdf0932842c0fca8ad6aaa3 \
+ --hash=sha256:563e4568217dc907a91843f38c737be865222c0400a38cdcd0d26ce92b3db271 \
+ --hash=sha256:581b27663c6e9f4df68068f32fe6d1cd7647b31fac90237221a66f8821c342eb \
+ --hash=sha256:58a1b0ec4cbb930e69669f9771715b2c7898d3cdf064d9811f7a66afef96b544 \
+ --hash=sha256:5cc5d3a29f9ec86ce406e5ec09c241dd8dc4d30e838f74f68d728b89131a3acf \
+ --hash=sha256:63d38e9a9a10a20fb57593742e63c6b1e78dd7f6ef5472de8e0b1e4cf4f3db26 \
+ --hash=sha256:6b1ac7f1bc6c0dbf90684b77571a51a21b2463909fd916ce0ac9bfc4d566dc75 \
+ --hash=sha256:6b900073e7b8481ef1aaf4f6c1789d210a1db01a9da8789821578cfeb4c2d540 \
+ --hash=sha256:6c12d0393a903b58bc5f5a7406d6c5290acfb8284290d68547ce620c06f7d133 \
+ --hash=sha256:6e2780e33a58a93f27cc3bb74a55bae6f9a8278a1dbabdff392940d30d381671 \
+ --hash=sha256:6ebd39ee26db460cfe5ab8b71a15d1149b289139a0d3981522757d6af620887e \
+ --hash=sha256:6f8b41299b203ce8f627db670cfea82067d9638853dbeaf86dccd93878879b85 \
+ --hash=sha256:6f9549ca354a1d6d6167c458a1f1b12147726b968f02dd64b6a5801dba91ae0f \
+ --hash=sha256:6ff0145b34610e57c9fae20df4e133c8d54266447387de6fcc0bdabfe4db4569 \
+ --hash=sha256:6ff5f0ed70783dcb9562dbd20edca51c3d4d277f128223709e3da6b75986d1d4 \
+ --hash=sha256:714bf348f468532d86bed670837e7d5ddff3834dd7f5d3c08066da400c86f088 \
+ --hash=sha256:757e3f79cb865a7db94e0db5f4d0ed3284a69e39d53568f433982ea13c60cac1 \
+ --hash=sha256:7e32b83bd8c2f8b6fa726ef34e63e21c4d7eddc277d40d4ef7245ea3ed28e5b6 \
+ --hash=sha256:805b0f2618e5d4c3e28f45b731eb1a0539691ae4a2f97b4ce014de0bf96a1ff5 \
+ --hash=sha256:80eae881cfb69383303e9a4d7961a478025b89c24f38f2e69b30c516fa0d57f2 \
+ --hash=sha256:813a32f94991b9627795528053c73a57d2ce3eb98ede89f0e1c7a31095938e81 \
+ --hash=sha256:8463b34ebde3f000627e9dbd8a545f995ad49fbf7ff9dd5abc0cd507da98a603 \
+ --hash=sha256:8a59c749a73fbdbc8e63b895a3079825fa085d752e75bc0a500042cb8a801e48 \
+ --hash=sha256:8d90d10e9b6594c28f27896a68fab97fd784c43804e9fe419dab8e8dcfcf4b02 \
+ --hash=sha256:8e1e037bb57dbc549c6fe20370b763ea74bdb09413cdcf857e4f14d9e4e2fb13 \
+ --hash=sha256:931f45f84e15daafec5f82cc92e6710569e1f50933f3253d206eab4132bec678 \
+ --hash=sha256:995b52f7c260ac7023640221f27472303968753cb6fc6fce1ddfb0e9db59a398 \
+ --hash=sha256:9b4da5789d7cf576c7e81f0088c632f6ee3786d87d17f08e90e703c22ce15633 \
+ --hash=sha256:a3ed60ea9a7c352c590182c67404599e6b5a0c901e75ae4cceee9a9fd6bfa455 \
+ --hash=sha256:a4d1ecad62e83cc65b411ea0125972cf3af98821e8117129947fd1e3a113f8d2 \
+ --hash=sha256:ae9bb62a7902e2ab65782447cd3eeb753510feace4e3ea03937a85489b01b16b \
+ --hash=sha256:b2ab3aad55d75d0b8df8d8a1b5920baaec9b161112cd5e95984848b4d2cd3dfe \
+ --hash=sha256:b2cc6991f16f6d666d48e4b57318104e7b29109e32e2f6b86e9d44c4e6a27f4e \
+ --hash=sha256:b5a3f5f70967a1aa2bc47fec42a1e19d2fb38c61700e3ee62b63a4af4f4fd001 \
+ --hash=sha256:b68fb053b37c258a473ab67f4965c3b439500dc160fe364667035a6833eaf50a \
+ --hash=sha256:b6ee42112d785a913dd63ec0335435a3dddbea5040c151252db815b0095cf066 \
+ --hash=sha256:b928ab0ecaa664e8caecc529dcb8bc881b6b35bb2b74bf9a39ae25f982ee8812 \
+ --hash=sha256:b9430f65db521db7962ad951571d446171213686f96c998a54dc18ed574821e2 \
+ --hash=sha256:b9cd15cb7cf0d5cc41f649fd789aae12c56c3b83eff593f8e095c1d4555ad5c3 \
+ --hash=sha256:bb1533541c729ad422f870a780d8b4af924f9817d45b5f580390418cda72eaa2 \
+ --hash=sha256:bbf7377fbd41b7c87d47820e25b9876724963681c2a1d6f6ff2adb4db46ac174 \
+ --hash=sha256:bca180cbe84e4fba7807eb408a8655295f697928512324517e30a091ede522a8 \
+ --hash=sha256:beb2c8a34cc90fb4d862b7284eafdb322030d6a8b2ee5eb6a744f84205beedc3 \
+ --hash=sha256:bfdabac0c6d3d6a5be8c2a100a001c92c14a39bbafd5999545a675c493626e64 \
+ --hash=sha256:c0e45def4d9ce7073e2226535572442d9d6efb4047c7a5fd8960807e877ce70a \
+ --hash=sha256:c0f537e5e8152e8d9cae82804024790cb973061abd3b7ef8f66f46e2b5c7bb51 \
+ --hash=sha256:c195a69df0ab2541252ab5b1d76e3c182e5688ac2a9b708e5e6f66aaeda91e9a \
+ --hash=sha256:c271bfb832be5c5c020b4e2fcbc1e70a0b990adba6de874b0bba1184b89cdea3 \
+ --hash=sha256:c42424213c28804f8d0e20f5692106cfb57bf72e1dbc4092b8481fb2f9e4c707 \
+ --hash=sha256:c4fa57d3c31889722f64bfa785545a5e603a893b6f29ac1a41bfa830abeaefd5 \
+ --hash=sha256:cb2bb3ac0af7fdab2311b895c9eb95442b45deb14cc949b9e65545e74aa0be69 \
+ --hash=sha256:cb3e7a4fd0168e362673a980380bf4fd6ae3b1555150e60c5390b4b10d9c50c4 \
+ --hash=sha256:cbbfcd5d15056fbd1edd5e725cf3feeb47c7cbccbe205927ebab422cc229f417 \
+ --hash=sha256:cd3e55223a77d6e08d5730ebacb4930ecca5d2ce7c57e7ba10833be7e52903f1 \
+ --hash=sha256:ce8e723b4637034b76f5382a30a6b725518c332273e8d62a6c7d46e90837c947 \
+ --hash=sha256:d1e329a1866981efe0201d05a374617f6c6cf14434a501d78ab22793d1ab1fa6 \
+ --hash=sha256:d20ba5c84cf0592afb2713336f07e2b6ced082e4ae803ceada153a85613efc9f \
+ --hash=sha256:d2b095129b9a98eb46a271ee9631089529c4e40354576b4aa74e24de9d2bf2f7 \
+ --hash=sha256:d3906b5c549ff2ad2473cb711e1fc65d76715c2726a402108fbf55eab6c6b49d \
+ --hash=sha256:d484ebb7e3a3f3597b0f645fbd1b85633674ca808c1f5ba11c2caf7c66f5c8b6 \
+ --hash=sha256:db735a23ecb0f0450d2b24e0a05fb00a8a35c9db172919c4d3e023e7c7ee4c9b \
+ --hash=sha256:dbc9fd1521e573045d71b6afab7398439c5cc259e8cb9d416fe62d485c4899c6 \
+ --hash=sha256:df3867518b205be3648e2fbd522bf380c851b5c2500588047505afdd786b6669 \
+ --hash=sha256:e0acbd474d0af4afacc6e66c4273f8a19e25f8af4379fc816388095ea6b01371 \
+ --hash=sha256:eacf0f45ca3ff84c01481c60c15da9ee56711f7292f66663df0f57af61e011c2 \
+ --hash=sha256:ead1a40543a033a6732a9e1e515944979a19db3737ce77363fc0660e38554344 \
+ --hash=sha256:eae4e9c7a0785a1a715de0a74fb822ab40084c060f444f18f075d05e322aa7ef \
+ --hash=sha256:ecf7037e491c220cd73987838c1ac3958d787bb098c3be0bfaf7f04204a6162c \
+ --hash=sha256:ecfeee649184ffd800955068be9a6b579a0f33fc3c98535d685d5779cb59347f \
+ --hash=sha256:edd5aa045fa3cc57143db018dd32ce7962bd5b525d05230709015d7e570100aa \
+ --hash=sha256:f0ef48ce353f6b6a52232ba23d0983d4c2c84c84a778899404e34b4718509bf2 \
+ --hash=sha256:f1734bd6f588975ffc246211e8b96c11933344087ca280d2cbcbf35cf835d7a9 \
+ --hash=sha256:f67db0ba2bedafec15b8e5330d40da1e1c7921559fa715af021252bfef81a6f8 \
+ --hash=sha256:f6ac1414556b910a879c108d79736f77e797871f9919ed0d2c3cf8cf3ecca986 \
+ --hash=sha256:f78f7ae1c2e5aabf29583fc0d302d8081a663776f84578025662eb6f5d63a921 \
+ --hash=sha256:f9489c1d87160c126f73b004742fe8654fa1ce37ed89e9e01330a1c10aaecde4 \
+ --hash=sha256:f9ccc9884241efceb4547a92955d128574c864681f11b7ea3ecbde295fafbe8b \
+ --hash=sha256:fc1a4f9d18d32a6e0a0a0a382986a60a2126f5144dd08715be7adb8df18e8a46
+httpx==0.28.1 \
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
+idna==3.20 \
+ --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
+ --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
+iniconfig==2.3.1 \
+ --hash=sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960 \
+ --hash=sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7
+kubernetes==37.0.1 \
+ --hash=sha256:a313a482361506340b5972cabaf20a7ee16ec8713e73325bf699b783a40464a9 \
+ --hash=sha256:e41db0a95615c2388680a2d556df33fd0f27a5717c9b5df5857e475c196c2f10
+multidict==7.2.1 \
+ --hash=sha256:02009b402129a73e1f3da68f48eb999afbacd29f25aef0d5052b84ff5c2786a2 \
+ --hash=sha256:02d9c04d395a36eeebe89e45f2269614a4675b8e83fddcba15bba2be903971e5 \
+ --hash=sha256:03846be88267369908fcd9a91159358a3b142d740991c96e4f696d362fce516c \
+ --hash=sha256:08ea94663d9e7c25d14d78222f7aee906c472d4837766bec62477c38f6e419dc \
+ --hash=sha256:0c454b677214fa604e9c4a38952c685dc88a62c96f5576a7ce550f213d1dc318 \
+ --hash=sha256:0c52fdd8cb8194fcb6969a417149aa1d09d6e48b69fb1b34ee1b1e4cb51ec183 \
+ --hash=sha256:0c7b4d8cc9e6ae7070b8129bdb840215552d4e2689ca765d37eb208a24919209 \
+ --hash=sha256:0d852d3d9638f852158d7de7200dd77f7ff8d77fc8f3d396032f3d847883f631 \
+ --hash=sha256:0eb7fa012ab47b13e96bffc1369642f520adcc4e3d3e170d7f1a2b39fb953bce \
+ --hash=sha256:0ec95a02f9f08246cc7f1d85ac425e4d5c233fd3f098f5be1b2cca16aa690186 \
+ --hash=sha256:0f30d6eaf3cb309b6c21096c6c2b4cf3800f195a78b1babae85f481ce851bdc2 \
+ --hash=sha256:12bbab225ce7f69df75409ab8005b11b4e9309fae863821e019e81b32e53a2a8 \
+ --hash=sha256:13268013a3db11a214ccb6920f778d175b765f69a40509610b92e43602272718 \
+ --hash=sha256:15da10cf11fd8d8716720c95b93180e94c8317405d89d08524c579ef360128ac \
+ --hash=sha256:1d369ed297e79b417766327ece16bb7e89306c588df63c364a85d90d4aeac843 \
+ --hash=sha256:1fb8c1a3480c130c3a6422d5f16f3cad6552bf08c2089dd288ba7134649183b5 \
+ --hash=sha256:2022bd3194968a027f64ed16f9779e62e71a823a9df3971a7328c1d45dfeb9ab \
+ --hash=sha256:21d86f2aa58aa428fb9909d6b8587b75eea827287e3515b8506dca86b55e2c7d \
+ --hash=sha256:223427392fdf993e198644ec40c404fd93f1cd2a61328d18842ecbf154d02c82 \
+ --hash=sha256:2529873a3bf68c372532a9f8058bcf4199e78387e92cd6a01b73334bfe6a7e3f \
+ --hash=sha256:2592fb6c335d67f1912bf72788c02fd5dab305fc3c8c7eab078a5bec1aaa0f88 \
+ --hash=sha256:25ebc4208d9098f77caeb16df895a0e56eb6c3b22870ac4e3ec43e0a9d6a33b6 \
+ --hash=sha256:27166b6ec73eac37696d66b2c6efbd44b6d04b597d655070959df54dc7c71c0b \
+ --hash=sha256:27cc258247b13008742f0e239933ad01dec5ed4e6dfc63849e30e04e1aa5bc82 \
+ --hash=sha256:295a15ccb8cfde9351dd6062b80af4e839460636b3c46813b4aa443905ded86a \
+ --hash=sha256:2b4d0c4e13d329ffee73d46128e32dd3b4aeb2f9c6ac9c8a89eb28d3b82dcea8 \
+ --hash=sha256:2bac371da2c3554a99ba126f545e79ab8e8d9426a3e41df81d00ad725c1e2cbf \
+ --hash=sha256:2c4e6a97f79699f33e6dc13bddb5631781ee79a4a94c1822116d100e7d5c136c \
+ --hash=sha256:2d4cfbbb1023f0f6795b5395c400752fb23a5a09ba756aa9003886bbc47b3e12 \
+ --hash=sha256:2e279ad5a6e9347bc9e043fdbee180a0480c8fd52618c3258e983dd93d98ed49 \
+ --hash=sha256:2f3cf710144b1859f4128051cc27ea86eee83caac5648cf2ca0c1b835261cbbe \
+ --hash=sha256:30572b61c7932e4d3c2ad1e580de7db1f35b7a35d36db38f89397ce3f2e69096 \
+ --hash=sha256:335f715f8c111654f29e4bf19086bdf0245579d2b4f4f59ca8dd9b3003b859c1 \
+ --hash=sha256:363e5db93fc3f65f3218a79fe108d217b9d264181a4e993c77552bfe275d0393 \
+ --hash=sha256:36b868758fb11cfa748c788b793994e73660abec90f83c780a678539c2c468bf \
+ --hash=sha256:37b161535e92776788957a165fcf38a80b33c1efde7b18cab287db4e4574df78 \
+ --hash=sha256:3b7542fad2706595c1b1853b46f2c3c4ae5cc5a6252b7225f294d607a29e1bdf \
+ --hash=sha256:3ca4e2e9102d0788b94894a30a5e3561fd4818a0605d8748d8fe780f890aa069 \
+ --hash=sha256:3ca60708f0ca9d994f46790c5a5c774771e2f1dcb75b12c70a5582f915d0e721 \
+ --hash=sha256:3d22ebb716ac79bf32c2a1d15250f017faef719114af4376e8dd814d6633cf0a \
+ --hash=sha256:3d6fce7cc4ef63d9a7633cbae43e737798412805a4c3ba876cdf472dc6ff9c51 \
+ --hash=sha256:4b338b45dc26ca4bac702e1278754f78613d4d0081e14e94e44fe8e261bf1750 \
+ --hash=sha256:4bebc9587cc29359e3b045518a7a09264fa882a7d165b0d143fdac8eeb7ea948 \
+ --hash=sha256:4d61be3725016ef5db3543c95f1f062d7d150ba776582d959a982af88e75fa55 \
+ --hash=sha256:4eb2650aa240b62ab0c6d87238bee5d14aa4d411c0d69ad61be53e026e0785ba \
+ --hash=sha256:4f31ddf9c00e0e7db848b0236c717acbe8a51954d0d31b4d908c538ab3f0f5bc \
+ --hash=sha256:522f7d2351743804cdf5c5a304bc45f9d55e935dd87706fae2dbeafe0fed0b49 \
+ --hash=sha256:5324411117bdb67cfaeb3d1d28fb783710fcca3d88b855696af99044d7219e4b \
+ --hash=sha256:554ee1d2e71fb5b4059c0c4ee35d8cbc8af2a3147036d65a454c53fea07ff4eb \
+ --hash=sha256:57ac987ec5d26d89bbc48c03e486f659e1367d425c465552ac2e4c4af8219def \
+ --hash=sha256:57b5d0df4c60ab8b1159687610a1c3ae069ad2380d390a79dedff5c82318e714 \
+ --hash=sha256:584ebfa26c9dcf3e465637a5a9afa2647bebb3022b50b1ff384834ba8e0380d1 \
+ --hash=sha256:5a7df927c7999d1d3196176a8d4139d3a0e1765936f5de8c768097275b2d617a \
+ --hash=sha256:5b944091cfa7da4f7a9bd2587569cd90c94b89ec4721f8e87fe62504f2432b67 \
+ --hash=sha256:5deadf03fb4a7842696d551f9f9a06dd4445bb43ca06b5f9736e4e1885561333 \
+ --hash=sha256:5efddcdbcb73f5485dca2b6624edc8ee063604b77bd3a3eb772826063965819f \
+ --hash=sha256:65e4448dadc7c94a8643bb41361e45f3a73d46e36a008a9bc03e9b525f4d7c80 \
+ --hash=sha256:66af55b07e4593790157e85f897f06983ded8a8692cb2fb048f3049c1d3fa172 \
+ --hash=sha256:678ac791b5820d9346eb13c8515fe0c9383659335e5cfc544bf6f54714ca1939 \
+ --hash=sha256:68b97b721d95b77d75e718d1b62a227fa4b3e647bd2a9fb39552d1a79ffb67c0 \
+ --hash=sha256:6966806cf917cbbeb9a8b5c581db52e19c9af0ef5fe009db54c1f5799a6e003f \
+ --hash=sha256:69befda4b1ef57027ea95dc6ed733568642f330fb3203a3e5c5ef107d41e0a1f \
+ --hash=sha256:6e22756c6e2d4f57ead0d2e6be179d8a16d661bee424b81de0c6c0c1305ec39f \
+ --hash=sha256:6efdeed2723431999cf3bcbc84c72dbe0f65d16bb1f84e15a657f5b60ff18ca6 \
+ --hash=sha256:714fe5f3eabd153b1b6911d0b5d31cfc2b808ae6b9e59a32fab17a4e6cdad5a6 \
+ --hash=sha256:7238f7135481926d52a94c18a8cca633a9e72c1cbfccd5796548298a479b0656 \
+ --hash=sha256:74d3e015c28b43b6c1a108eec324ea2efb185c359c459216cabd4cb98f7f02f6 \
+ --hash=sha256:76a4f5132b8dc29eb3c768ee9b47e132a1d24fbc2e9259854c01af9beaff1d49 \
+ --hash=sha256:7726c53b1d59eb57b009a9e8759d4f71049ebdacf30642a198a197f80cea451c \
+ --hash=sha256:781ca5dfc2f1d58e47f52216a0c1c7e1ba91ce8aa235459e20de2d402af77fb3 \
+ --hash=sha256:785ef2f3e372dabc131f228a275f97eca92ad17cfab1ab138790e330d99abe3e \
+ --hash=sha256:7cb1a6bb70edcc06ea65cb9a35654f268ee39d9fc3a52240e6ba074db4d9c99e \
+ --hash=sha256:7fb3bc8d58e1b7c1d9fa1d995e30a4ce9dbe49f4c63c755a1ffdecdfbd026ccb \
+ --hash=sha256:8148c2aaaf4b85d7872f024c8bb0c9b9797cc76b10588e3594a69cb01057472a \
+ --hash=sha256:81a5f84a76401c4330e6db4e170dddd58860b30c4e8a44a181e5c2809c7f9c9d \
+ --hash=sha256:81c6fc88ac9bf6920a0cdf22d3c383964167f12d35eb630463fc6d5ac89e460d \
+ --hash=sha256:822a7fcd33f6922b650e1b26594f09d53c5d011fe655fea7ded27b2728ce2a1b \
+ --hash=sha256:83fbd7cfaca614975efdbdcd9e2df45f3e987e819216468fb73e1db6f0365b98 \
+ --hash=sha256:846b17193f1c373d6f05ada894fadf56bce2473067caea0aab51828849894b31 \
+ --hash=sha256:8648dd5dc4d5db32b5936dc5e85e9a4d79db1135513d40d1f54ca15503750324 \
+ --hash=sha256:873d35366cc115be02a1c858f9b22a1a9f6182b99aab2171ee5276af02a153e3 \
+ --hash=sha256:8c3ff4f5c376df7126695a52af61e6084b69c9e52dcb9f4713e71ce9819e1bfd \
+ --hash=sha256:90efd90a0f9b7ae3844a4fb3ae614321832d364ae19567b7cd312315fac4ec39 \
+ --hash=sha256:9306e0c5595f91874e685be0a9eb743d48ba069c888eea051bec3c67c08b5b8d \
+ --hash=sha256:987f65f6c9321402bc7012f6c6d09ba501b88ddb1640f5b0b4a19f2caafe24a5 \
+ --hash=sha256:9a53776adc1ba923c9e345fdea7b61553c44c17a9af5eba8d8def8c5dcac4dec \
+ --hash=sha256:9c44bd978adddf39df5834c6a1efd7d075c84769a7a084168d035a2a276d9008 \
+ --hash=sha256:9c9f285f788acf2448a57c9b93d31103ac69bd03f92907e2ffbb23c4e2b42eb6 \
+ --hash=sha256:9df532dd12c59a7a4d381646fc1bf5ccb4bcdbd01c0887d07ec769fae965ea9a \
+ --hash=sha256:a0f999155c22c8929cc492949c08c6fe7ec8079a5e1bfdeb34b9a3e4991959d0 \
+ --hash=sha256:a2a322c8a6ca82a5a67ae6542e9c6a13ded9045cfd171dcccc5c213ac4fe0eab \
+ --hash=sha256:a3d28ac22fe658ad7486d1532b3a9084fbf65b428db31f47212c5b0b6da826f1 \
+ --hash=sha256:a3d9a59bcdf8531d46b56bcd512017533510c25e955c2c546efd0743bd840940 \
+ --hash=sha256:a789e3d27fc5665cc0477f10a35a89383313e4c9cf8b5e6af7ea71f039cdcc0e \
+ --hash=sha256:a7d120bdde5b213e67ed7dc80ea3adf4f014453fbc20195d619ba2ceb34084f3 \
+ --hash=sha256:a878fef2797a7c082aa47559af68299372edebc7367bd5128dff2d36e6d820e1 \
+ --hash=sha256:a92e3080d8889b0f7a80ea0c4a72181b4d4d5d3918a03136b4a459f51f5f59da \
+ --hash=sha256:a9cc121b5ee2c8b4bd4b6b526d5a26bb782f4d620463f73707691b2004b8c349 \
+ --hash=sha256:b4821bd30b38144d8ab75df56af0b07e9d71327c9cf94c0c7a1965f865bb9615 \
+ --hash=sha256:b5fb6b048931ea925b484f0c7c01b007981415a00a6c62ee74bbf8d229d39c08 \
+ --hash=sha256:b63acdf4df5a78a15223bbb510bb3e0f8234dfa807610ccb8796d0842db2c801 \
+ --hash=sha256:b81ff0f2d375988152614eaea4d5023c98ecb08f77df4bb1d96762504bb86fca \
+ --hash=sha256:b97bffa5772b7e853303de094829fcd3a9bd49fddd70534651ee4933691f6587 \
+ --hash=sha256:ba5e8d9310a794b853fb0e327d46a827b5b015576341219a591f88ed9d799c27 \
+ --hash=sha256:bc9decb36abd00ed5d123db9df70eb9a0da45d6376d86d64f4a94bfd5a1b83fe \
+ --hash=sha256:c10ac5e3e76837180656079008f71add8339b00c3c26314092bcd1e8e449049f \
+ --hash=sha256:c753b820738cddc097892fee95e7cd716fbdfc9640414c5e9ce9463ad125321b \
+ --hash=sha256:c85a0a54a19429a17ef449e967baffe7b5089cb4f6ef2894c9168a69fa024d05 \
+ --hash=sha256:cadd02ca804d6ebe799a7af19197d9730094fe2cc74b70eb4f374f4b8c1886c1 \
+ --hash=sha256:cb43bb34346155d479acdc5423afccdff79ce1fc66adfcc9de995cbec8a13acf \
+ --hash=sha256:cb7fc8820b206932ec6d8f8985aeba6edfd2932e13b6c4bf2434d6bcbbeef3e1 \
+ --hash=sha256:cba29f12cd717f1cbbe220edf005666f8df0453347217748331a0c2f7522eca4 \
+ --hash=sha256:cd69c99affd1b4debc1f9a53058ea6e3a9b34f2e733840bb45891666cd8ede0a \
+ --hash=sha256:ce0f884408938fbd72e764031d0e23a8f5e438a7bf73641089c7a6465cb84456 \
+ --hash=sha256:cecb2c778f00a65c6ca01410dd194d0e797a9eebb7c6dea0db87718719f293c7 \
+ --hash=sha256:cff7657e79224dea46f2f2e58eb99186656ca63ebe356212210c5c00e9beb814 \
+ --hash=sha256:d0381074dc21225951278d7e4a7a2241f7fb86a4d8b94154dfee1411f49cf851 \
+ --hash=sha256:d094951489bb7836342c005e790bd005e60b669aaf5c936b96dd5d68cbd2e23b \
+ --hash=sha256:d10e49b940e70e2a4e91f44098007dda0bacb78c3f8e31ba7a37ee2ce52269e6 \
+ --hash=sha256:d37773029ba034a2cc970b6eeb67dad3bff9d894393c51f6bb1c6b4f563a032c \
+ --hash=sha256:d4af0d863e72f3ea48e85d3aa1d1b426fd81e9544fd7976fc6d6753584a25b03 \
+ --hash=sha256:d5738cd929f2af8651ebb284467acb58b1ec0f2f4d42489cd0597172bc82e15b \
+ --hash=sha256:d615377a8e7bfc4f1983ede3f411c2ac2b0f7c3053f262d0437b7d8bcc4485e3 \
+ --hash=sha256:dbda2baafe08f8431fd3333be02c261023fd454607e3bde5e7dc00565f7bebcb \
+ --hash=sha256:dc6f3dcb08c4b41e64f2d6cf272b5686d9144db585b2e138a8b1664376ab3921 \
+ --hash=sha256:e0f2b33a6ae233f4da38c77ea32f90aab2cb82ba819d7b6c4f1b68cd33d871b6 \
+ --hash=sha256:e47d058c2db95010f5d2e1f6037acebdfb0e2fa4507650e1041437985c91db38 \
+ --hash=sha256:e64b86d16158954825d12799271979c6944c1fd1d03575b2f22e869c3f7d00dc \
+ --hash=sha256:e8af3e6f1bdbc31fd762b094133b478b89988eaebc63cbe82b1d9e3bbd15cede \
+ --hash=sha256:eb5c4a5b1281985271b6c29be53f2c1a9decf11e30a0e2733439c4d9d61b9ebb \
+ --hash=sha256:ec4ee5d2d624088775e61cada87bbe7629bfd936bfbd57f122a106970d8ab1ee \
+ --hash=sha256:ed6c107642a9acaa7ede32dcd428a6c87e302c5d555ecd3ec132ffa8c6155b4b \
+ --hash=sha256:edffdb3950d3a8b769ddaec3a0962fb7b76b3c96f317f6b88500455a537dded2 \
+ --hash=sha256:efe695cc2081633b02fcb95bc1b13070f6647c9ad52d42a8beeb3c2a7ea062dc \
+ --hash=sha256:f07f06db2e333b68dd52560b8b0b12b6f8bf6053cedebf9af4aecfecd2dc1b32 \
+ --hash=sha256:f15205c67d4c45250fcff5da077f0ae54102b5bc39e9ee8bab90d99e09a728c5 \
+ --hash=sha256:f2087ee23a18d1d618a135eedcafba7dfc40c6be4e8e52d7f29a1c8a2344cfcc \
+ --hash=sha256:f278f118f9fdb5faba5a6792be397fbda094d1568efa4dfe8c3595ce4ce89eea \
+ --hash=sha256:f29885fef157a602d1816439e5da964c70ebcdbbcd4bb9d2d21b7e8bb5636604 \
+ --hash=sha256:f2cc27ec6a53b1d39bab44d2b9dd21cc31e796d01b5e18a76d569721282bb264 \
+ --hash=sha256:f332b16911ffc668baec6aaf8f08a09e1af34a8ee87ccac26bd722589e264645 \
+ --hash=sha256:f5812cfcf7daf3ace5fedf6e2ef8d4b342c92fb8ea3a87c1aed2b50453271093 \
+ --hash=sha256:f62172cf387e92f4928b60e5936b02c06e3c8449ed7fb440978d6c9bcbfc85df \
+ --hash=sha256:f9c118e866164ce05498d9106a4bd01e745321403cd6f0b77a0b2587768f8c06 \
+ --hash=sha256:f9e78d98a29e969a001e953eb09167a48bbc71249e804dae5db987ee7e6531c9 \
+ --hash=sha256:fa0ecd064b0ae18d235283abb5adbc4e07304dba8f2aa7c0d61f28bebcc70d4c \
+ --hash=sha256:fa949c81e3865c10f981dea72fd8d418b59edd13a047a0aee89ae64790113d53 \
+ --hash=sha256:fba33e264cdd2ec43b762b9960d1648478eecfc1ce6e8159e7573510f0412c0a \
+ --hash=sha256:fc45ff19520bc5321dcd9c6dd7df6f35ddd894eed35e5be65d1828a9c3f5e7fb \
+ --hash=sha256:fcfebcb537f419e415b6e2bfc77ddbd33abe3c036f0105410a25a14741df9c31 \
+ --hash=sha256:fd24c4425fdb8e822601335602375c6d0cb020f7a368e1d5842eb7925a9c8748
+oauthlib==4.0.0 \
+ --hash=sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd \
+ --hash=sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86
+packaging==26.3 \
+ --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
+ --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
+pluggy==1.7.0 \
+ --hash=sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec \
+ --hash=sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8
+propcache==0.5.4 \
+ --hash=sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5 \
+ --hash=sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432 \
+ --hash=sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72 \
+ --hash=sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97 \
+ --hash=sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf \
+ --hash=sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983 \
+ --hash=sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77 \
+ --hash=sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1 \
+ --hash=sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c \
+ --hash=sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284 \
+ --hash=sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827 \
+ --hash=sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b \
+ --hash=sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8 \
+ --hash=sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745 \
+ --hash=sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae \
+ --hash=sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb \
+ --hash=sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320 \
+ --hash=sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d \
+ --hash=sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b \
+ --hash=sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec \
+ --hash=sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4 \
+ --hash=sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c \
+ --hash=sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc \
+ --hash=sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a \
+ --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b \
+ --hash=sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9 \
+ --hash=sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b \
+ --hash=sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea \
+ --hash=sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51 \
+ --hash=sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831 \
+ --hash=sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba \
+ --hash=sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad \
+ --hash=sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56 \
+ --hash=sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f \
+ --hash=sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25 \
+ --hash=sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2 \
+ --hash=sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d \
+ --hash=sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d \
+ --hash=sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d \
+ --hash=sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd \
+ --hash=sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a \
+ --hash=sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c \
+ --hash=sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111 \
+ --hash=sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e \
+ --hash=sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac \
+ --hash=sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c \
+ --hash=sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760 \
+ --hash=sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57 \
+ --hash=sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7 \
+ --hash=sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993 \
+ --hash=sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3 \
+ --hash=sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3 \
+ --hash=sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757 \
+ --hash=sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc \
+ --hash=sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51 \
+ --hash=sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0 \
+ --hash=sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838 \
+ --hash=sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826 \
+ --hash=sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8 \
+ --hash=sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589 \
+ --hash=sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef \
+ --hash=sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8 \
+ --hash=sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468 \
+ --hash=sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177 \
+ --hash=sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab \
+ --hash=sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487 \
+ --hash=sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5 \
+ --hash=sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788 \
+ --hash=sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece \
+ --hash=sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792 \
+ --hash=sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62 \
+ --hash=sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d \
+ --hash=sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f \
+ --hash=sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47 \
+ --hash=sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944 \
+ --hash=sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21 \
+ --hash=sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5 \
+ --hash=sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737 \
+ --hash=sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e \
+ --hash=sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12 \
+ --hash=sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161 \
+ --hash=sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e \
+ --hash=sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9 \
+ --hash=sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9 \
+ --hash=sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb \
+ --hash=sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526 \
+ --hash=sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d \
+ --hash=sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc \
+ --hash=sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef \
+ --hash=sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9 \
+ --hash=sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b \
+ --hash=sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370 \
+ --hash=sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162 \
+ --hash=sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546 \
+ --hash=sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1 \
+ --hash=sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f \
+ --hash=sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f \
+ --hash=sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38 \
+ --hash=sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894 \
+ --hash=sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429 \
+ --hash=sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8 \
+ --hash=sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a \
+ --hash=sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680 \
+ --hash=sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d \
+ --hash=sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c \
+ --hash=sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574 \
+ --hash=sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8 \
+ --hash=sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867 \
+ --hash=sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6 \
+ --hash=sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30 \
+ --hash=sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4 \
+ --hash=sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9 \
+ --hash=sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f \
+ --hash=sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937 \
+ --hash=sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3 \
+ --hash=sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031 \
+ --hash=sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39 \
+ --hash=sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2 \
+ --hash=sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e \
+ --hash=sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9 \
+ --hash=sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c \
+ --hash=sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e \
+ --hash=sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8 \
+ --hash=sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36 \
+ --hash=sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0 \
+ --hash=sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565 \
+ --hash=sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c \
+ --hash=sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858 \
+ --hash=sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5 \
+ --hash=sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97 \
+ --hash=sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123 \
+ --hash=sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9 \
+ --hash=sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874 \
+ --hash=sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68 \
+ --hash=sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2 \
+ --hash=sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572 \
+ --hash=sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a \
+ --hash=sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558
+pydantic==2.14.2 \
+ --hash=sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118 \
+ --hash=sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d
+pydantic-core==2.50.2 \
+ --hash=sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179 \
+ --hash=sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41 \
+ --hash=sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057 \
+ --hash=sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d \
+ --hash=sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d \
+ --hash=sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2 \
+ --hash=sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7 \
+ --hash=sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99 \
+ --hash=sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd \
+ --hash=sha256:1a5e859f5a8a6d834b0a13acaa0eaddf1a9b4b015fcda13bab7b471e18299d32 \
+ --hash=sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910 \
+ --hash=sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775 \
+ --hash=sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9 \
+ --hash=sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7 \
+ --hash=sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97 \
+ --hash=sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72 \
+ --hash=sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f \
+ --hash=sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40 \
+ --hash=sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58 \
+ --hash=sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d \
+ --hash=sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b \
+ --hash=sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc \
+ --hash=sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376 \
+ --hash=sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b \
+ --hash=sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1 \
+ --hash=sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe \
+ --hash=sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a \
+ --hash=sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756 \
+ --hash=sha256:3939538e640c94ab779a05f2d45dafedd01d65d93e1290797f3ac5b59fcb6455 \
+ --hash=sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a \
+ --hash=sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80 \
+ --hash=sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f \
+ --hash=sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d \
+ --hash=sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6 \
+ --hash=sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca \
+ --hash=sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a \
+ --hash=sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7 \
+ --hash=sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb \
+ --hash=sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3 \
+ --hash=sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af \
+ --hash=sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c \
+ --hash=sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986 \
+ --hash=sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4 \
+ --hash=sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04 \
+ --hash=sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9 \
+ --hash=sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f \
+ --hash=sha256:5c12a5a080570a91bbcbf6d10db008b80e3f5dea015ef2f0137bbec70f24f3f3 \
+ --hash=sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4 \
+ --hash=sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead \
+ --hash=sha256:6258754c473b3045a7aa30b833a8a331a1ab36c6147cb230789861d80bb26f83 \
+ --hash=sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a \
+ --hash=sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef \
+ --hash=sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b \
+ --hash=sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3 \
+ --hash=sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798 \
+ --hash=sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234 \
+ --hash=sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02 \
+ --hash=sha256:701bc55d08c22f981973ca4f6fdf4ea102ee1ee3a21b818b66fdb161ea711a4a \
+ --hash=sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11 \
+ --hash=sha256:71615edeaf1ae70f5f3f558426072eede0168b7e5190fd9633878e29a0b85732 \
+ --hash=sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92 \
+ --hash=sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e \
+ --hash=sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6 \
+ --hash=sha256:7f9821dfc0c473c96ad55ce4189c7cb4f9c94e7d4f563afb874173243dec1162 \
+ --hash=sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108 \
+ --hash=sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432 \
+ --hash=sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8 \
+ --hash=sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601 \
+ --hash=sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780 \
+ --hash=sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544 \
+ --hash=sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46 \
+ --hash=sha256:89213368eef163350af37127939ace099010e8102757905c3e329f1175950ed8 \
+ --hash=sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2 \
+ --hash=sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02 \
+ --hash=sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc \
+ --hash=sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d \
+ --hash=sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b \
+ --hash=sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d \
+ --hash=sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd \
+ --hash=sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437 \
+ --hash=sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853 \
+ --hash=sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a \
+ --hash=sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f \
+ --hash=sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d \
+ --hash=sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960 \
+ --hash=sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3 \
+ --hash=sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977 \
+ --hash=sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3 \
+ --hash=sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324 \
+ --hash=sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b \
+ --hash=sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c \
+ --hash=sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0 \
+ --hash=sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5 \
+ --hash=sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee \
+ --hash=sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63 \
+ --hash=sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636 \
+ --hash=sha256:a786cf782ebb2e45a43c7118d75d452aac982b79577dfe804d8dbf2878f0b589 \
+ --hash=sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c \
+ --hash=sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed \
+ --hash=sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf \
+ --hash=sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd \
+ --hash=sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb \
+ --hash=sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f \
+ --hash=sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51 \
+ --hash=sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285 \
+ --hash=sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314 \
+ --hash=sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe \
+ --hash=sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d \
+ --hash=sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf \
+ --hash=sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31 \
+ --hash=sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371 \
+ --hash=sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71 \
+ --hash=sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e \
+ --hash=sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f \
+ --hash=sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d \
+ --hash=sha256:bf7f99a0e6616cea754e2334d129091f3260d011369d9b8c658522bfb2427619 \
+ --hash=sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8 \
+ --hash=sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b \
+ --hash=sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0 \
+ --hash=sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6 \
+ --hash=sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b \
+ --hash=sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f \
+ --hash=sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a \
+ --hash=sha256:d71a9a9352acbcb5c73d39dd003383599428067b6eda5cf7c5d335a7a0da37e1 \
+ --hash=sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773 \
+ --hash=sha256:d93c066a38f83376b5bc020e563666784d1b680a70d6ceb625fdb541af7a312e \
+ --hash=sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b \
+ --hash=sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738 \
+ --hash=sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45 \
+ --hash=sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680 \
+ --hash=sha256:ea71b94a2eb5d2fc102926321f56d2ee8eddfad2673eadb1d23e9f287a492715 \
+ --hash=sha256:eec187d2876abb6a74203efbe2aa796455c1809334acf4e739dde35da8cfb69f \
+ --hash=sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329 \
+ --hash=sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9 \
+ --hash=sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db \
+ --hash=sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23 \
+ --hash=sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0 \
+ --hash=sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848 \
+ --hash=sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd \
+ --hash=sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c \
+ --hash=sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67
+pygments==2.21.0 \
+ --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
+ --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
+pytest==9.1.1 \
+ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
+ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
+pytest-asyncio==1.4.0 \
+ --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 \
+ --hash=sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42
+pytest-cov==7.1.0 \
+ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \
+ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678
+python-dateutil==3.0.0 \
+ --hash=sha256:3c79630202e4152c6add678446984236efe4f57e84d8e892aab0637bab82e9fd \
+ --hash=sha256:7ebcb3c29a92f3ceafe79a7a48a5c45677e9e004bbeec5742ac0bbb00560b7a2
+python-dotenv==1.2.4 \
+ --hash=sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc \
+ --hash=sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0
+python-multipart==0.0.32 \
+ --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \
+ --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23
+pyyaml==6.0.3 \
+ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
+ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
+ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
+ --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
+ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
+ --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
+ --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
+ --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
+ --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
+ --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
+ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
+ --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
+ --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
+ --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
+ --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
+ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
+ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
+ --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
+ --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
+ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
+ --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
+ --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
+ --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
+ --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
+ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
+ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
+ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
+ --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
+ --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
+ --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
+ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
+ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
+ --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
+ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
+ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
+ --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
+ --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
+ --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
+ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
+ --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
+ --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
+ --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
+ --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
+ --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
+ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
+ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
+ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
+ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
+ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
+ --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
+ --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
+ --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
+ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
+ --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
+ --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
+ --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
+ --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
+ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
+ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
+ --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
+ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
+ --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
+ --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
+ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
+ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
+ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
+ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
+ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
+ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
+ --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
+ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
+ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
+ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
+requests==2.34.2 \
+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
+requests-oauthlib==2.0.0 \
+ --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \
+ --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9
+ruff==0.17.2 \
+ --hash=sha256:122258f6ecde0e0dd1bb60b24263c3edde4338caf723fca711cc729f9bfd414a \
+ --hash=sha256:195ad918027f817c119310ca89a42d347887dfc28934aaa869325e9c5dc9d962 \
+ --hash=sha256:1b59d447f197bef9ef48a22ba1a6016137753c6099e9bac9c36eb566e422424c \
+ --hash=sha256:2d086be452eed686aa51271ecf2cbe5037956517cc6ee39a17b4489ac12f8bbe \
+ --hash=sha256:39c5b55c960515cdb32be16e69efb5734c589c06a0eb38a22694524a336b889d \
+ --hash=sha256:583bd67bb9e7061dd09bc46a0a884bae2bbd214dbcd48e509813612619683869 \
+ --hash=sha256:5875a16b0de70b43cb81eabea5ae5e8f5d1b2fd1b5e6ed07f80cfe5019d0bd5a \
+ --hash=sha256:6bce7d1f5165701ae18f5df85c65afe78cddfa6c5fb4b9bdb0014d60388f3edd \
+ --hash=sha256:7cfd783590edf2b77317882631900f6a13ea2e8eacefe6ebf2370d2715299160 \
+ --hash=sha256:7f8fba429d6ac4128ab29cfb8fa6240d2fa671d04adf7f84d3b22fc7b6b6bea4 \
+ --hash=sha256:84d632402d14c23b488f941db23712d6736a013225162062d34558c3f3c780eb \
+ --hash=sha256:8c21b4ab24485358f17291a4f8dc5399b834e963b8efedd920fe4fdb02081bb4 \
+ --hash=sha256:92a80d2f44584dabb0189b3a6c393e37e525e04942838482010575cb81945be7 \
+ --hash=sha256:96a65e755d8dba094707fb23ab602111d9a31ec2835b3a6992a1914a8fcb211e \
+ --hash=sha256:b7d0906677d89bbf3cf73cde64b5bbe2ab35df3ae43e49ee2627301e91265bac \
+ --hash=sha256:ba405cbd5ec7bda1f555d5e52be1fd68d02608d4d95d24591bdac92cfacb579f \
+ --hash=sha256:c555a877f8f41447e318101cfefea9bf12dfa9f82eedf90c376e57e069f917d0 \
+ --hash=sha256:c60f93fec7142dba5b8f487f94bf2f6231dae24dc11e4c48648347c8b0720004
+six==1.17.0 \
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+starlette==1.10.0 \
+ --hash=sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4 \
+ --hash=sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b
+typing-extensions==4.16.0 \
+ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
+ --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
+typing-inspection==0.4.4 \
+ --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
+ --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
+tzdata==2026.6 \
+ --hash=sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec \
+ --hash=sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d
+urllib3==2.8.0 \
+ --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \
+ --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63
+uvicorn==0.54.0 \
+ --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf \
+ --hash=sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620
+uvloop==0.23.0 \
+ --hash=sha256:0305871ac712f54b62af73f943dbf21ae3ce80a44bc0f0151424484affa85645 \
+ --hash=sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208 \
+ --hash=sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4 \
+ --hash=sha256:0efdd55bddbd36bb2fcb842d64c0d5f6407c6958c68088cc25df8c09edc5b5fd \
+ --hash=sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc \
+ --hash=sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5 \
+ --hash=sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb \
+ --hash=sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f \
+ --hash=sha256:24c58ae4a83e93a04c504bcc678125e36a0bfc44af928ad69444880c60f187a5 \
+ --hash=sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27 \
+ --hash=sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65 \
+ --hash=sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330 \
+ --hash=sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55 \
+ --hash=sha256:42feced24b9b44b856c633eafb5cc5dec354972da55ce77598db6844c054bc7c \
+ --hash=sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63 \
+ --hash=sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8 \
+ --hash=sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f \
+ --hash=sha256:4bb7f5d0b62b5afaaaea2b7b60d508921c24b0fe39c22c1438bec1811ffe10ec \
+ --hash=sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027 \
+ --hash=sha256:514698d3683189031dcbfdc31e87115992e5ce9e1b19fe5359941323f2df800c \
+ --hash=sha256:53c2c5d7e2024e46776c2d90e6c637d01102126b61aaf5faa5edaf05f8b5722a \
+ --hash=sha256:55d6f4135d914305929fe9e9c44d8b5383a9b3fa1bee3bfcf60ee97e01af07ea \
+ --hash=sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254 \
+ --hash=sha256:5a3e0f56ec19bfd9ad1605572878dd6ff7f01b325f4fc154812ae70d615c3aff \
+ --hash=sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d \
+ --hash=sha256:60ec798c40a1810d282ee046f61ecac1c5675cb898763d9f08d97d53a5e00a81 \
+ --hash=sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e \
+ --hash=sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405 \
+ --hash=sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f \
+ --hash=sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507 \
+ --hash=sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208 \
+ --hash=sha256:80cac5cb90ed7b9b72a217a1d6982b15b829cdbd0ee6bc19b93e3a9e47fb0ac9 \
+ --hash=sha256:8af88fe5c7dd68fe1fec6dea8155caa1a47155d219a750ff34049541cf536a5e \
+ --hash=sha256:8fcd721113260ffb5e38bf14a8725b17d431f34209f7d1c7005b667946e630b3 \
+ --hash=sha256:93087a845cdfb35753e539354ac9551bdd2ff528c202a98df0ae46e852bcf021 \
+ --hash=sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3 \
+ --hash=sha256:9bf08e4b6362dd1c08623bbfa2d061e8bac0f1da8fc2007062cfe1dc360a49fa \
+ --hash=sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d \
+ --hash=sha256:ab17b3a8aa754be0de0e397f7b95f13b14e56f077a4c6ae295e3d4afd199b325 \
+ --hash=sha256:b0d106d9314546d69b3df1b5352639aa628530ec3ecef8a98a21942d2a2a64f5 \
+ --hash=sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd \
+ --hash=sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49 \
+ --hash=sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac \
+ --hash=sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476 \
+ --hash=sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53 \
+ --hash=sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a \
+ --hash=sha256:ce17bc317d089f361b33521654c13e30eacfd3d2034fd34e613ca9c51c969686 \
+ --hash=sha256:d918d6f304a309222a784bbd140b85ec5594d97e4dc0e79f590549d28970663a \
+ --hash=sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848 \
+ --hash=sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5 \
+ --hash=sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb \
+ --hash=sha256:e49eba8f1e28e7c03648b7a476e1ba05309e087ccdea859fc6dd659564aa8d7e \
+ --hash=sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d \
+ --hash=sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410 \
+ --hash=sha256:f50b580fad005a092ed87c5a3a4683459b21d1620497d6a5bccad203bee4c071 \
+ --hash=sha256:f5576e8ae1723ece60d8f93c6710abf784714e99388bcf023ba9ca800bc587f6 \
+ --hash=sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2 \
+ --hash=sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda \
+ --hash=sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f \
+ --hash=sha256:fefea5cf8cdda9053b962ca8a90216fb0b1d40907dcb6819382b42e483e6e9f6 \
+ --hash=sha256:ff7144d8167e513fe39fbb46bffb4f6f192dfb1f4b0b4e9102e1fd4f212e4747
+watchfiles==1.3.0 \
+ --hash=sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441 \
+ --hash=sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95 \
+ --hash=sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4 \
+ --hash=sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61 \
+ --hash=sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977 \
+ --hash=sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509 \
+ --hash=sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1 \
+ --hash=sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5 \
+ --hash=sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661 \
+ --hash=sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef \
+ --hash=sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f \
+ --hash=sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c \
+ --hash=sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30 \
+ --hash=sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80 \
+ --hash=sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e \
+ --hash=sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207 \
+ --hash=sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0 \
+ --hash=sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef \
+ --hash=sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1 \
+ --hash=sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c \
+ --hash=sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29 \
+ --hash=sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7 \
+ --hash=sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935 \
+ --hash=sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3 \
+ --hash=sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39 \
+ --hash=sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc \
+ --hash=sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26 \
+ --hash=sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a \
+ --hash=sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6 \
+ --hash=sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694 \
+ --hash=sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca \
+ --hash=sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a \
+ --hash=sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3
+websocket-client==1.9.2 \
+ --hash=sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0 \
+ --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce
+websockets==17.3 \
+ --hash=sha256:018fab26140ce221c3853454a608e4e7004f4f10641cb97819dcd5b917bb2e99 \
+ --hash=sha256:07246d4f0293ef0b87eff3d7dd88c1f03dd9f678fa39f6de87b2af8504e7e0de \
+ --hash=sha256:078d1d41f45ee34eed5a4f3bbe7c588caecbdd6e12dfca73e8c1b0e803773793 \
+ --hash=sha256:0824e6ecee838cc28e6481062574c7fde49f9a2cedfacd36f0e91cb4b37fd6fe \
+ --hash=sha256:09f007b2150eff015b2109f006e2ac2012d376f41486defa88ae78a776b00cff \
+ --hash=sha256:0a0bcf66289b9d017c175562c35c5304b5cc3ed56aded994ae30b0a9654ba405 \
+ --hash=sha256:0a657b51e4a5f4ac577c11e706ff9a884ea5aaf3e2666bae283a251c63e66acb \
+ --hash=sha256:0c63ad657695e8f1a48624754cb6b464118ac50d4c1d477c26461c98390f0a62 \
+ --hash=sha256:0d20d6ea5aabbeba2e054d33e405192ef75dc12947bbd00ec97d23b14969d325 \
+ --hash=sha256:0e65260a1b1d7b81651f47343d836de70b88b02358ae3658efa6382104ec37b1 \
+ --hash=sha256:0f78bb2e82fe88912c9d7b25c98e57a9b08655579af89f164fd4c0657a075d62 \
+ --hash=sha256:10b24e836ee0233ca078e1244f6ba40343a22127a9a736f46a7e7b18de2cc711 \
+ --hash=sha256:13cffaedde6e06183860fc92d88fe1d190abda73837fe4c14f5d4803c472f5dd \
+ --hash=sha256:13eace1b315345ebac5b27821f5dcce24e5cf9976ab18f79e00a59bdd67e5d19 \
+ --hash=sha256:15decc0fe293d9e3f924bd483f3d76f3d0ff26845112a470e3f8842fec2c82a1 \
+ --hash=sha256:1c7dd93a1c9f89297471493674b1fc1d9467cafd45374f2172213046bdb3a329 \
+ --hash=sha256:1dae5cd6c92d4440b15f20aa2987baa73c47ea14baa0fec8536a542e82e0ab33 \
+ --hash=sha256:1db2604e6aedb7a2cb2f6e0575d6c9a852ccac8239703a930ac83b63309177a5 \
+ --hash=sha256:2129c9a5fc05a230ce088745a790af073e582b584badc7b05db067ee326f56d7 \
+ --hash=sha256:23046098ebf844753cfd211033624a8247520e5bf2194c24cb1a8c222917acff \
+ --hash=sha256:23cb6f6d64e3a389bb8ba150f947f9a6c8c0aca383e814ca3c5cffab738d5646 \
+ --hash=sha256:26838e2d95d7004c52e8c7b6b51676c872093a3b55a497824f899116a15a8a55 \
+ --hash=sha256:2789d203babb6ceb2770aadc2394c1f99263ec37fba57a93064f9a8ad899e8ff \
+ --hash=sha256:28d1935c16782a1e3650c9baa79069b016fff739e292e4cb14bbb830d6930491 \
+ --hash=sha256:29b01520c39ef2f23c88b002f11e6ef4165675f6c92f5f3578fd587d69b5a5fc \
+ --hash=sha256:2a377c7132b1e69be7a50d7434d7f26f69e93c70166ebb6d8f8cabf802ac4506 \
+ --hash=sha256:2b06213c8e1ce93b18d0476d7a5e647436c8f23d099888c9065e930e80f4bed5 \
+ --hash=sha256:2c460aeb12c2bd1c204944d9e9199595bf9e505eb542018d304bf9e63cb60cad \
+ --hash=sha256:2d08ca4bf5a9cdb6fd81b86b51bd5cfb10527d3a1199888f2b02cc6c6a3d5f80 \
+ --hash=sha256:2d875224bac1a05a20acade268a7a0e3498e7f8cf890acb7c2b23bacd2bd3659 \
+ --hash=sha256:2e99440ef446c7833690c1754e23b0188d440f3cb89812b0764c697a7880cb33 \
+ --hash=sha256:2fa79b9a48ae59744316c71a34cb9f76f30c93e117338522c5135cb7eac10351 \
+ --hash=sha256:32029441af953e980b9940f042b337fe4ad9a97fa8b17a4ed878ec67b5b89ef4 \
+ --hash=sha256:33221e49be79d7bfe6d12bc52ef9458b256ab74fa8ac5660acfa3065de1c7870 \
+ --hash=sha256:34c78052277053bcb09810ac8e9ba4029f9e3e277833fc18ec1c928a6246ac91 \
+ --hash=sha256:35381b575f70a08d4241e73c0cde4672bb9b2a9aae8767ca6b7fb61305a052c8 \
+ --hash=sha256:35fe35d20baa661cef4a12ee7bc42eaf2376d43ef9816ab29dd47ca957f7a73a \
+ --hash=sha256:39f532fa0d9026de4b9315a175998646112776baad54d55348435c11b05d034e \
+ --hash=sha256:3c4f631dc8d00e470cde3ec0a35d4b58a817bea592411956f1cf4099dbb306b3 \
+ --hash=sha256:3d78d93846eddc990551c1c31914d90d71957ca3af468969826f7ed0d7d1d51c \
+ --hash=sha256:411aa0fb234f42a1444dea1e177890ddfc577b36ffdfafec25cb0d2c818e1af8 \
+ --hash=sha256:41665c7ea01d3ae497a0c61fd22d3424230d18782a5fceb79f04db9ccb2042c9 \
+ --hash=sha256:4170556762c6fd2913d99dfdd8dbb4c927584899df7facb1034627e355263c45 \
+ --hash=sha256:43f270a5f21e12db08c73d83849a877c02ab7b7d0fb6e2718b5101d9c810212f \
+ --hash=sha256:443fa28652682d2a3ec3eca49af1b87cb2523561257683e62e03a98967b22138 \
+ --hash=sha256:45cc1ccb12ced6d242335159e8221733c8696da5ad0b6ceef6debb0f5ce89110 \
+ --hash=sha256:46ae05aa1aecd47d4d7de0f05ce2d1d2eb418f8de614964d754cc3b9a409c400 \
+ --hash=sha256:49fb5b4960ebe1bc557b435da8687deef1302a08f030a33c5606fa6fb11b773b \
+ --hash=sha256:4a9133dbaedc6f1af358127275676171d9a1bb3e61f47ee62b2ff9acb78af3d0 \
+ --hash=sha256:4aa85b34c21cf97b3470793d0bf0763d32bfd1e67b83067a0a3ba8dc3474dacf \
+ --hash=sha256:4d5f2492cb2192aab2d42e57f2caa2159b4fd20fde06f859ec13354f524b176c \
+ --hash=sha256:4e7b5d23ef144b32645d97bd99f382c4394c443395f45fe1576786838d26e308 \
+ --hash=sha256:4f7431e71072962d3ed1768038200aa4f1e76d9c2d7bedd554d57a55faaf6644 \
+ --hash=sha256:508775efa11d26aec186d10154359f52276e2baf9ca201b52356330883276dfd \
+ --hash=sha256:5142798871a248b422f353e0684d51dfa119de3403a6fa0904b7cb806d9790ce \
+ --hash=sha256:52a2ca6f401fb49a8d5d9e0e48ade5f58f1bdda33132907b0e4e3b702628fa4e \
+ --hash=sha256:53209dffd91b0aefed9ea630fa6ad5c2944d6210ba3a8a3a8664dc1575289b14 \
+ --hash=sha256:534ed583fd4323de45d196535a444cebd1af2105e6e4e0cdcbf8cea266a8747a \
+ --hash=sha256:54fcd24944e6d127d198c28dc363967b7131ff0c97833962c2ef2048018de02d \
+ --hash=sha256:55494475206113dbaf731dc8c5ffd5e1094175b753b7bbaad09fe3d7df61165c \
+ --hash=sha256:5670a762c7ae388bf8aa04199fd436180a4d28318dca9c1eae7b38604676dbfd \
+ --hash=sha256:56a76556ab04ab54a675acce2cac8b4e74ea2cec558a7c9ed37e0367b04b5d50 \
+ --hash=sha256:56d43c311f3519fb1057b6114dc24e9060bc3fb6aedf4aa11160484154b66b63 \
+ --hash=sha256:5868a02c4f6d698ddd19b4c9742748c03dec045289915806845189d96bc3d314 \
+ --hash=sha256:58e5d1e935a90878a935c7d2737a25c71cf4f1c13b6e6bef454ab722c4f338bb \
+ --hash=sha256:5ad409b7ea102339041281117cf2dadfe71edbb653dc48037cc55d516fb7f448 \
+ --hash=sha256:5af7070d80d278832cbca9c3c3dd44b30a26dcfa3ae1e0f6f19ff34edc27b689 \
+ --hash=sha256:5d0c871efc696ef5eb8514df64b86ab28736db753ebb42a2101f3b562f4849c0 \
+ --hash=sha256:5e0c0ed37472c4729f3001066510a656486f08f149dda1ebfd3dfe4450c9756e \
+ --hash=sha256:5e1c174b366565e101f903f9d1cc96d5508f805759dd37d012743f1c73ec0d7b \
+ --hash=sha256:62059d739af7fc035ed6e63c5390118a60a5e9aa5a5541cd0486a83e741842c7 \
+ --hash=sha256:623d4b44f64efdf235b1e8dce266057073d93cde6197e7d7684f20c297f67f30 \
+ --hash=sha256:628efe13630ac3397e7d1278b6be6f22f37a86d638a324334b4cd43c1daf9928 \
+ --hash=sha256:6342f79d6f50e680c8aa59e0615d745e867e51ffd59c77093be60b7c433067af \
+ --hash=sha256:63b87c93e28957e6dc2bb92d31727a863c9bf4a4daa080f0995fdcb0242c9166 \
+ --hash=sha256:65e937576141ac50fac32fa9736dbb3e0e4a7769f91be004011a09216c6ab05f \
+ --hash=sha256:66027e401c70d0cf55021b91b8a7ec2cd5810e20602f945b4fe6ae8b37ed232b \
+ --hash=sha256:6644eecd99c4e8cdf72e646e17b391bb70b054f44becde37dcdc48cbec7e1527 \
+ --hash=sha256:6705af9d1005127302c04d9f1533ba56e289336c465923ef878430e99295a0b9 \
+ --hash=sha256:6a8248b1ba8861e48e4c7ced3895a1264d2a3b617de72ec94172ce6e64bf16c7 \
+ --hash=sha256:6b14914e67044724c31a9360c7b89e9d070117c630746eefdc1620d967545acd \
+ --hash=sha256:6b7b540bb321f7f0a7b8f5d107f941bb361e3a154c0b594dae0394997b91b743 \
+ --hash=sha256:71250e010eede33c1d638e2c7a98288520d67f782fd5eceecaeca3129233a09a \
+ --hash=sha256:722ab747ff5dc28070b1a8c020dba6466f13f2ab56bc251ad301fefa2454aa1e \
+ --hash=sha256:724224d4c457207271239114bf43afafc3eca4a1b4661ccdd428d11869a03bbd \
+ --hash=sha256:729989759feb81ff6493879a0d60a0bd7d2eee41001bbec0381a080318439382 \
+ --hash=sha256:740bba4223fa5216d4d6c5dcd29e697f37229b8a61b69e1a50527b0ea993ab2c \
+ --hash=sha256:7bdc658c9474864893e0b6b94f1390bf6a912952df1870dd93bb7d1a902b5628 \
+ --hash=sha256:7d53fc4b90ef6e11de639dc18744d9be8cbd70ccb5ce80027aaffe7b1ab0215a \
+ --hash=sha256:7fa69212ea60ea24e2d8aff23fe102b1f87f1b27a320f92d2e7b3468997a496a \
+ --hash=sha256:80326bc571164bbbb6bead2e35d0c943b23b24fde629032197c58151529d24d8 \
+ --hash=sha256:80578e91764bfca297ddca322c00a3136d993b829e9839e7fa378a19c1fcc25d \
+ --hash=sha256:80a6e37e235780f189558ab804f3cbc01c18f5c578995739b49613d544d34ebf \
+ --hash=sha256:8135a549e27e9941a70e2d2156676ef2ffeff4b06e9d564dbaaae963482afdef \
+ --hash=sha256:82e60c00cfb8493a5ba27cdf9b3695fdd153f666f73e11f18968f78ead1d8705 \
+ --hash=sha256:856bbb24f2ee6436af7609267e73309e4bb0a0f9982f79f1c2e01019279c6671 \
+ --hash=sha256:85d6daa3a7ca404570ae130babbeb347ef9d3faca5300fb13cfa4bb48c3d30d1 \
+ --hash=sha256:8879b6a4040ee925c638548238ab7fe612cf5fa8752fe847346da0da8be81c3f \
+ --hash=sha256:8b3bede7d583bc9f47d8693a29984882e59077db1a5238ad977fc70ae8169741 \
+ --hash=sha256:8dbb3862aa649de95660b6337d97101977f160b5b354d07b82fe309874d2c8bc \
+ --hash=sha256:8f244dfee05ea29b8064701880865eb1684f3a704a5665bd10b64694444dbbd4 \
+ --hash=sha256:8f7469408740cf06a79b01605f2ae0b2acc8839c03fcbbf05da49baee12d3302 \
+ --hash=sha256:90cc9973c51811918247a78d9ab2f54871296e4e37e69a98aba4d3c9acedbf62 \
+ --hash=sha256:90d8c4fa5ba293cf92de351fbc292cdaadb1a1d58d031a1a97ff9b8fb6250ec7 \
+ --hash=sha256:94c02b0702656f083c098bdbd93bce22a7aa7a2b34ba56d2472efb67dab7cf58 \
+ --hash=sha256:9662a9fe457820c60365b3209e03cbd8e4a817195d55de65a80fa680b4bea136 \
+ --hash=sha256:997617dd7856c5dad742bf6893b872319156dd43f47587541a59e4a852d52bbc \
+ --hash=sha256:99c5637b74e2805ee4de9e2a43c8b45f678e50abb1c2da9880f0372f6498ba2f \
+ --hash=sha256:9a215749c9a88ef2842e880215193e56ae60b856773c99702b078236bc6d4adb \
+ --hash=sha256:9bde5d82d7ac4859e0cb3a35e148f537a8d4e56aa43f42582b5f263982f095ae \
+ --hash=sha256:a004c1fe5a72c496f28c6c1d8c2dc5cfe3278d99478e18f8b838c70a912b366f \
+ --hash=sha256:a0e2e7c2c1a4f25db7b6596738d810c10732e77f852247300a1bc49e364d6dc9 \
+ --hash=sha256:a22c4956a4d858200dba83f0691bb692d77b0eeddfca80157820d20b85dea4f1 \
+ --hash=sha256:a7f391838e8adcf1f2fe71561d9e5a9f74ea7ffa5052eb2db329aac677383136 \
+ --hash=sha256:a9eacb382f400bb5865e7e6d24bbe709a84510291e707f169dd4a24ee4a5ac6a \
+ --hash=sha256:ace8dd1efa5f44c482d091aa38322e20e3ffe5adcdba6b67ad9f4e7da58f3f68 \
+ --hash=sha256:ade76f8c84ade325bbed96806357fffd353fcc29a5166d6dc6d39381f6d9f76c \
+ --hash=sha256:af37a1e6b6bd506b597d5fb4cf111fb527d1b2223608a5d441d4738827cfd166 \
+ --hash=sha256:af7c474f0d8f4d2a196370e85033873c7b063a0b5da9a4f143aa023e3bd25407 \
+ --hash=sha256:b225c656efa2dedecc81651d2290170af25887b7de4f38c5efc7c5ae2b9442ab \
+ --hash=sha256:b459d29c8b82a420a98fa0ce8e9cf787ce0062d98cc90604ef9574fb3cc26ff8 \
+ --hash=sha256:bc6f87d03e3691154673aaa8f08991723d6e3394c83ef2875c7a9049eac9595c \
+ --hash=sha256:bc8fd230ef013951900c1fda100eebe4a4b1b407dde2a44186995042dda2c628 \
+ --hash=sha256:bd645b5d2f810a181c44f4a71135e7c365b44f4be9ed3d1892635be13a857553 \
+ --hash=sha256:be7be57097a65a8fab3d0f0a10d12931deb626a7d8082eb732b1ae41f3a202e3 \
+ --hash=sha256:c15a165673bb3fb57facbf040edf5c0556d20ccd2e0ccc5f23ebb69fb715fdc1 \
+ --hash=sha256:c4b3f2c7d84f303da873b634d0beef83b5e1137294c3de4e23102841dac0cf4a \
+ --hash=sha256:c534c9524a2acd0b97b7022074bec3784ac273c683cf9535087e437672ea8531 \
+ --hash=sha256:c95d25b5382ee0e6191ecafdeabefdc1f7bff8dfee29239b9b79e760ef3b04bd \
+ --hash=sha256:c9d06edfd974e918b40e0662a7e76dac13125529ee5ec9fbe42dc0d6faa209c3 \
+ --hash=sha256:cdb8e4b0f6a41b857c74c03116e8ffbef59ef8e533e934b532843178990d01d5 \
+ --hash=sha256:ce39283ef08b0f28478e920d4d89a1fc169176e568e8a6a821332673145522eb \
+ --hash=sha256:cfef7f7aa434a1e632e0ebda14b39f4f1d7975e0fce5c77ea7b43156fabf9bd8 \
+ --hash=sha256:d2605a3b2af8d66f8b75312d223642c065ec773446c04566d9030ad5e2db13c9 \
+ --hash=sha256:d338813a440bf6d2f0df55140d3f3080284410b1ebfe9df50e3847bcdb1a17bb \
+ --hash=sha256:d355b22270be8062360b9df2ee0b10277590789a1059257916fb9e256a0cff80 \
+ --hash=sha256:d68da281ac208b6236ffa8beef23fe7c7c956d8bf09b421d447232fd94441b35 \
+ --hash=sha256:dafbb58a149d1ebd5767b3f064f911c5ee0897608005e51c3904b0ada2a8d99c \
+ --hash=sha256:dc2b495a92b3971966bf102e73589f950db3ec1ca6713265355ce7771d97b199 \
+ --hash=sha256:dd0f3037d501178a761fd2814370aca547f79e99a200a61a1f6e7b79ec3d78b9 \
+ --hash=sha256:de2c12555bf05546f3ee75733f2c81de70aab7f530e86d0d2162b0ddcb3c39ae \
+ --hash=sha256:e164c4a6868d11988c7f2386e3278ccff21c665cd7d08279b62709dd37a3167c \
+ --hash=sha256:ed59cb4ee258674a6ed9b8515e42079db02cb3be94f133fdd3f200183a40c49d \
+ --hash=sha256:ef07e0b6b879f699a16fc7a513b974246fdeaef44908a021130cf4afe6a1c89c \
+ --hash=sha256:ef2950896673a9809e8e5ab857525924fbcacfc98b858b0509ccf50d341f80f4 \
+ --hash=sha256:f09f145b89ea292cf6c25fd7c8c0d90207fd6a9e54c7828499669996a1e5bdcc \
+ --hash=sha256:f27e7e8a85512b6a4b490d5756edde8e6772883a5800e1476172e44eda3f7d7c \
+ --hash=sha256:f300f28dff74ff4148c21e6971e4de6ca0c34e0086a3d95c017c5174ebf85ca5 \
+ --hash=sha256:f3ed747b7651668131029c720fbc32c7ee2d129bdad200e6c8f4037b20f7913a \
+ --hash=sha256:f87f02afe05c1c246012e3de100a77cfbabd17db709fe93cf074271ab239b312 \
+ --hash=sha256:fa87ea2ad64e59fe5622c1dc5716106b6d6e7e4b26b848cb5f5710a75c292ef5 \
+ --hash=sha256:fc23e72bd7f8a8e540fed2244524d0a9aeb6ae3325f3d56f04a7a3debe5b04c2 \
+ --hash=sha256:fdd00937f15f5f93c8e53d8060a8c4c010431ab595a9b5b50ddd569a04603944 \
+ --hash=sha256:fe7a168d805c8a7bf1c2de55b7ab2bc958586bfd88d0e87235a92cac66b590a7
+yarl==1.25.1 \
+ --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \
+ --hash=sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7 \
+ --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \
+ --hash=sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa \
+ --hash=sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62 \
+ --hash=sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec \
+ --hash=sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26 \
+ --hash=sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651 \
+ --hash=sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453 \
+ --hash=sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6 \
+ --hash=sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc \
+ --hash=sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287 \
+ --hash=sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426 \
+ --hash=sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868 \
+ --hash=sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde \
+ --hash=sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9 \
+ --hash=sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7 \
+ --hash=sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0 \
+ --hash=sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf \
+ --hash=sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95 \
+ --hash=sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5 \
+ --hash=sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6 \
+ --hash=sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94 \
+ --hash=sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a \
+ --hash=sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57 \
+ --hash=sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c \
+ --hash=sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee \
+ --hash=sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b \
+ --hash=sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b \
+ --hash=sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c \
+ --hash=sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26 \
+ --hash=sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c \
+ --hash=sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184 \
+ --hash=sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8 \
+ --hash=sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58 \
+ --hash=sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b \
+ --hash=sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d \
+ --hash=sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2 \
+ --hash=sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c \
+ --hash=sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f \
+ --hash=sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3 \
+ --hash=sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa \
+ --hash=sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3 \
+ --hash=sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb \
+ --hash=sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707 \
+ --hash=sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c \
+ --hash=sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345 \
+ --hash=sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40 \
+ --hash=sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492 \
+ --hash=sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371 \
+ --hash=sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec \
+ --hash=sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c \
+ --hash=sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c \
+ --hash=sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25 \
+ --hash=sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac \
+ --hash=sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6 \
+ --hash=sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3 \
+ --hash=sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459 \
+ --hash=sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03 \
+ --hash=sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40 \
+ --hash=sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8 \
+ --hash=sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209 \
+ --hash=sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54 \
+ --hash=sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d \
+ --hash=sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126 \
+ --hash=sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d \
+ --hash=sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9 \
+ --hash=sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0 \
+ --hash=sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225 \
+ --hash=sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414 \
+ --hash=sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059 \
+ --hash=sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d \
+ --hash=sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233 \
+ --hash=sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b \
+ --hash=sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4 \
+ --hash=sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273 \
+ --hash=sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc \
+ --hash=sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865 \
+ --hash=sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc \
+ --hash=sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4 \
+ --hash=sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1 \
+ --hash=sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3 \
+ --hash=sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee \
+ --hash=sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a \
+ --hash=sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df \
+ --hash=sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f \
+ --hash=sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d \
+ --hash=sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449 \
+ --hash=sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e \
+ --hash=sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d \
+ --hash=sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d \
+ --hash=sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35 \
+ --hash=sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420 \
+ --hash=sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b \
+ --hash=sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68 \
+ --hash=sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe \
+ --hash=sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd \
+ --hash=sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b \
+ --hash=sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8 \
+ --hash=sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9 \
+ --hash=sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908 \
+ --hash=sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85 \
+ --hash=sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f \
+ --hash=sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64 \
+ --hash=sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab \
+ --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d \
+ --hash=sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca \
+ --hash=sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1 \
+ --hash=sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7 \
+ --hash=sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3 \
+ --hash=sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa \
+ --hash=sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57 \
+ --hash=sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2 \
+ --hash=sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743 \
+ --hash=sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e \
+ --hash=sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487 \
+ --hash=sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17 \
+ --hash=sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0 \
+ --hash=sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f \
+ --hash=sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6 \
+ --hash=sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3 \
+ --hash=sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad \
+ --hash=sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d \
+ --hash=sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1 \
+ --hash=sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89 \
+ --hash=sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc \
+ --hash=sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f \
+ --hash=sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3 \
+ --hash=sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a \
+ --hash=sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4 \
+ --hash=sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98 \
+ --hash=sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258 \
+ --hash=sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b \
+ --hash=sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f \
+ --hash=sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b \
+ --hash=sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326 \
+ --hash=sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc \
+ --hash=sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75
diff --git a/examples/dsh-agents-as-a-service/gateway/requirements.txt b/examples/dsh-agents-as-a-service/gateway/requirements.txt
index 9b1892a..5a3f9d2 100644
--- a/examples/dsh-agents-as-a-service/gateway/requirements.txt
+++ b/examples/dsh-agents-as-a-service/gateway/requirements.txt
@@ -1,6 +1,1581 @@
-starlette==1.10.0
-uvicorn[standard]==0.54.0
-httpx==0.28.1
-websockets==17.3
-kubernetes==37.0.1
-python-multipart==0.0.32
+# Hash-pinned runtime dependencies for the example gateway.
+#
+# Generated file -- do not hand-edit. To change a dependency, edit the pin
+# below and regenerate with:
+#
+# UV_CACHE_DIR=$PWD/.uvcache uv pip compile requirements.txt \
+# --generate-hashes --python-version 3.12 --no-annotate --no-header \
+# -o requirements.txt
+#
+# Every package and every accepted distribution is pinned (== plus --hash).
+# The gateway image installs with "pip install --require-hashes -r
+# requirements.txt", so an unpinned or unhashed requirement fails the build.
+aiohappyeyeballs==2.7.1 \
+ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \
+ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472
+aiohttp==3.14.5 \
+ --hash=sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1 \
+ --hash=sha256:038c2c7e8caa26b6c8423779b5eaf1893904048a512c19b32fe841ffa5592b50 \
+ --hash=sha256:0684952aeae1f5dbfe02d46039338513b94009baecd15d8e4098a357c4c4a2a6 \
+ --hash=sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f \
+ --hash=sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d \
+ --hash=sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210 \
+ --hash=sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a \
+ --hash=sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f \
+ --hash=sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb \
+ --hash=sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e \
+ --hash=sha256:1220353657ad49493551f089ce02f1a348fd57ffd585bfec77f2f3c4fe3a7346 \
+ --hash=sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1 \
+ --hash=sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9 \
+ --hash=sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51 \
+ --hash=sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155 \
+ --hash=sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863 \
+ --hash=sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd \
+ --hash=sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30 \
+ --hash=sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b \
+ --hash=sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738 \
+ --hash=sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec \
+ --hash=sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7 \
+ --hash=sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382 \
+ --hash=sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6 \
+ --hash=sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f \
+ --hash=sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf \
+ --hash=sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584 \
+ --hash=sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20 \
+ --hash=sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12 \
+ --hash=sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789 \
+ --hash=sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658 \
+ --hash=sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c \
+ --hash=sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781 \
+ --hash=sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2 \
+ --hash=sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111 \
+ --hash=sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57 \
+ --hash=sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4 \
+ --hash=sha256:330900acd0dc4cb8b27f9c127fbaad770964845338493e7906ae3822e82dbf8d \
+ --hash=sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab \
+ --hash=sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c \
+ --hash=sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748 \
+ --hash=sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d \
+ --hash=sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e \
+ --hash=sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0 \
+ --hash=sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79 \
+ --hash=sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae \
+ --hash=sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3 \
+ --hash=sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7 \
+ --hash=sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421 \
+ --hash=sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793 \
+ --hash=sha256:4f07fe3ac408d8b3f768be471dc3f56d43843c47d97c66120534467a15ead197 \
+ --hash=sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0 \
+ --hash=sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016 \
+ --hash=sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045 \
+ --hash=sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178 \
+ --hash=sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622 \
+ --hash=sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480 \
+ --hash=sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f \
+ --hash=sha256:5af42135fdfebdadbc2bcd9c0842a48ccf0d62794c36a260b21dc4b94d1e0119 \
+ --hash=sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3 \
+ --hash=sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29 \
+ --hash=sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939 \
+ --hash=sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3 \
+ --hash=sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c \
+ --hash=sha256:602c1e9b718a3275c580149f947e7fac65044c0a20e599553fb12e9700da9eca \
+ --hash=sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef \
+ --hash=sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d \
+ --hash=sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343 \
+ --hash=sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d \
+ --hash=sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905 \
+ --hash=sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc \
+ --hash=sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67 \
+ --hash=sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0 \
+ --hash=sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794 \
+ --hash=sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835 \
+ --hash=sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d \
+ --hash=sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d \
+ --hash=sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c \
+ --hash=sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e \
+ --hash=sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da \
+ --hash=sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9 \
+ --hash=sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115 \
+ --hash=sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c \
+ --hash=sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84 \
+ --hash=sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc \
+ --hash=sha256:8df7d481654ac96fe1ba9a02a9f67770fdd367823e0d5ef01b922725c4bd2cfa \
+ --hash=sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f \
+ --hash=sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4 \
+ --hash=sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5 \
+ --hash=sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c \
+ --hash=sha256:96a2e584f0b9ed8f1fa33211397dcf67bb7069866402cb405d191c2f0defb9a3 \
+ --hash=sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a \
+ --hash=sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14 \
+ --hash=sha256:9bab2045550c4fe0f7baf89574db1b455c195750702ba96fef1f16972b146617 \
+ --hash=sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4 \
+ --hash=sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d \
+ --hash=sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2 \
+ --hash=sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820 \
+ --hash=sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed \
+ --hash=sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd \
+ --hash=sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593 \
+ --hash=sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac \
+ --hash=sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833 \
+ --hash=sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee \
+ --hash=sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3 \
+ --hash=sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600 \
+ --hash=sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d \
+ --hash=sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703 \
+ --hash=sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764 \
+ --hash=sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99 \
+ --hash=sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a \
+ --hash=sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755 \
+ --hash=sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84 \
+ --hash=sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5 \
+ --hash=sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3 \
+ --hash=sha256:b7806e804889231b0e06469fd4a5c06313d1c0a3377322b6d9237fa5e0fe4167 \
+ --hash=sha256:bea559ad70218d230663e4210875735076a9bfea5994cef34a55a25faeaf2544 \
+ --hash=sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1 \
+ --hash=sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946 \
+ --hash=sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e \
+ --hash=sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4 \
+ --hash=sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02 \
+ --hash=sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba \
+ --hash=sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02 \
+ --hash=sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3 \
+ --hash=sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a \
+ --hash=sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412 \
+ --hash=sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e \
+ --hash=sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec \
+ --hash=sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319 \
+ --hash=sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2 \
+ --hash=sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11 \
+ --hash=sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073 \
+ --hash=sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f \
+ --hash=sha256:d94e44be379e569758fee8a9a58431cfc3c2598c708b92b1cfe96c66b4c94aef \
+ --hash=sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603 \
+ --hash=sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2 \
+ --hash=sha256:dca3fa8d8a0a26679862eccb0b1a9151b2b9f1cd2c212e7a6335778faaff5833 \
+ --hash=sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450 \
+ --hash=sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a \
+ --hash=sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a \
+ --hash=sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59 \
+ --hash=sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db \
+ --hash=sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329 \
+ --hash=sha256:e95c8def4b81c5d68d5cf1f54c07acd7c0d2577af244e5b6da802120825737c6 \
+ --hash=sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1 \
+ --hash=sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8 \
+ --hash=sha256:ef692a24087a699c0a4a26af45e746e0c1eae2116f6d8a5ff91d8aae2b867b45 \
+ --hash=sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b \
+ --hash=sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09 \
+ --hash=sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e \
+ --hash=sha256:f2ebb54b3f932210503072f09974b4fb574d823e497a944adfdcd140a6a00255 \
+ --hash=sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1 \
+ --hash=sha256:f375db73a39f5cf83696d500e21a67f418dc9a988955756f254be8f03b7b3651 \
+ --hash=sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91 \
+ --hash=sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8 \
+ --hash=sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d \
+ --hash=sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677 \
+ --hash=sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534 \
+ --hash=sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441 \
+ --hash=sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550
+aiohttp-retry==2.9.1 \
+ --hash=sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54 \
+ --hash=sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1
+aiosignal==1.4.0 \
+ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
+ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
+annotated-types==0.8.0 \
+ --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
+ --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
+anyio==4.15.1 \
+ --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \
+ --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94
+attrs==26.1.0 \
+ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
+ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
+certifi==2026.10.23 \
+ --hash=sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd \
+ --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
+charset-normalizer==3.5.2 \
+ --hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \
+ --hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \
+ --hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \
+ --hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \
+ --hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \
+ --hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \
+ --hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \
+ --hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \
+ --hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \
+ --hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \
+ --hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \
+ --hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \
+ --hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \
+ --hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \
+ --hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \
+ --hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \
+ --hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \
+ --hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \
+ --hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \
+ --hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \
+ --hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \
+ --hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \
+ --hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \
+ --hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \
+ --hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \
+ --hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \
+ --hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \
+ --hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \
+ --hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \
+ --hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \
+ --hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \
+ --hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \
+ --hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \
+ --hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \
+ --hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \
+ --hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \
+ --hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \
+ --hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \
+ --hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \
+ --hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \
+ --hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \
+ --hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \
+ --hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \
+ --hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \
+ --hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \
+ --hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \
+ --hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \
+ --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \
+ --hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \
+ --hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \
+ --hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \
+ --hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \
+ --hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \
+ --hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \
+ --hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \
+ --hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \
+ --hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \
+ --hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \
+ --hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \
+ --hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \
+ --hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \
+ --hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \
+ --hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \
+ --hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \
+ --hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \
+ --hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \
+ --hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \
+ --hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \
+ --hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \
+ --hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \
+ --hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \
+ --hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \
+ --hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \
+ --hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \
+ --hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \
+ --hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \
+ --hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \
+ --hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \
+ --hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \
+ --hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \
+ --hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \
+ --hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \
+ --hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \
+ --hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \
+ --hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \
+ --hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \
+ --hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \
+ --hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \
+ --hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \
+ --hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \
+ --hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \
+ --hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \
+ --hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \
+ --hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \
+ --hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \
+ --hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \
+ --hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \
+ --hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \
+ --hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \
+ --hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \
+ --hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \
+ --hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \
+ --hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \
+ --hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \
+ --hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \
+ --hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \
+ --hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \
+ --hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \
+ --hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \
+ --hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \
+ --hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \
+ --hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \
+ --hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \
+ --hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \
+ --hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \
+ --hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \
+ --hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \
+ --hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \
+ --hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \
+ --hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \
+ --hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \
+ --hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \
+ --hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \
+ --hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \
+ --hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \
+ --hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \
+ --hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \
+ --hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \
+ --hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \
+ --hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \
+ --hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \
+ --hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \
+ --hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \
+ --hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \
+ --hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \
+ --hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \
+ --hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \
+ --hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \
+ --hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \
+ --hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \
+ --hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \
+ --hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \
+ --hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \
+ --hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \
+ --hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \
+ --hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \
+ --hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \
+ --hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \
+ --hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \
+ --hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \
+ --hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \
+ --hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \
+ --hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \
+ --hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \
+ --hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \
+ --hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \
+ --hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \
+ --hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \
+ --hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \
+ --hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \
+ --hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \
+ --hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \
+ --hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \
+ --hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \
+ --hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \
+ --hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \
+ --hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \
+ --hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \
+ --hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \
+ --hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \
+ --hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \
+ --hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f
+click==8.5.0 \
+ --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
+ --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
+durationpy==0.11 \
+ --hash=sha256:181898e1ae282e288f0a2291829656bf1b6b3aadf30a97993b85db4943642905 \
+ --hash=sha256:a739fe2b8972c250ff72f8e2c488d18cf25f7b852f49ee76048775d5171df30c
+frozenlist==1.8.0 \
+ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \
+ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \
+ --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \
+ --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \
+ --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \
+ --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \
+ --hash=sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84 \
+ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
+ --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \
+ --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \
+ --hash=sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967 \
+ --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \
+ --hash=sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4 \
+ --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \
+ --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \
+ --hash=sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9 \
+ --hash=sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3 \
+ --hash=sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd \
+ --hash=sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087 \
+ --hash=sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068 \
+ --hash=sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7 \
+ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \
+ --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \
+ --hash=sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f \
+ --hash=sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25 \
+ --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \
+ --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \
+ --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \
+ --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \
+ --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \
+ --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \
+ --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \
+ --hash=sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675 \
+ --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \
+ --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \
+ --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \
+ --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \
+ --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \
+ --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \
+ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
+ --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \
+ --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \
+ --hash=sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c \
+ --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \
+ --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \
+ --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \
+ --hash=sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5 \
+ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \
+ --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \
+ --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \
+ --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \
+ --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \
+ --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \
+ --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \
+ --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \
+ --hash=sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95 \
+ --hash=sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1 \
+ --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \
+ --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \
+ --hash=sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6 \
+ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \
+ --hash=sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459 \
+ --hash=sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a \
+ --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \
+ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \
+ --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \
+ --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \
+ --hash=sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186 \
+ --hash=sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6 \
+ --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \
+ --hash=sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e \
+ --hash=sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52 \
+ --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \
+ --hash=sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450 \
+ --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \
+ --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \
+ --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \
+ --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \
+ --hash=sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178 \
+ --hash=sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695 \
+ --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \
+ --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \
+ --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \
+ --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \
+ --hash=sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61 \
+ --hash=sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca \
+ --hash=sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad \
+ --hash=sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b \
+ --hash=sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a \
+ --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \
+ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \
+ --hash=sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011 \
+ --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \
+ --hash=sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103 \
+ --hash=sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b \
+ --hash=sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda \
+ --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \
+ --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \
+ --hash=sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e \
+ --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \
+ --hash=sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef \
+ --hash=sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d \
+ --hash=sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567 \
+ --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \
+ --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \
+ --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \
+ --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \
+ --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \
+ --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \
+ --hash=sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a \
+ --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \
+ --hash=sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47 \
+ --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \
+ --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \
+ --hash=sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f \
+ --hash=sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff \
+ --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \
+ --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \
+ --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \
+ --hash=sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581 \
+ --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \
+ --hash=sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565 \
+ --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \
+ --hash=sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92 \
+ --hash=sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2 \
+ --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \
+ --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \
+ --hash=sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93 \
+ --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \
+ --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd
+h11==0.16.0 \
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
+httpcore==1.0.9 \
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
+httptools==0.9.0 \
+ --hash=sha256:02bc5b3dcb6394b9d825fd62a7bfa0b2943063a3c89abc4492ad45e334a20eb5 \
+ --hash=sha256:050f7ab098121873c8f13e35857f97ab60a76185c8302bde9a384939bb7c3b96 \
+ --hash=sha256:050f84b7ec46a6efe0e5f521cf8729e3397c1cef4384f62ed8d5d68ca0045776 \
+ --hash=sha256:06bfe7fad972a417269d8a5fc53b87e4eca970354abf5e9e24336fd06d64292e \
+ --hash=sha256:088de1738e1af624466a01c35d652dbe6fb825be887c76d68aa850621d81db88 \
+ --hash=sha256:0adc974916efe1fbf89d0363a86dcb2c746727643e362ff398de1a4b50b6bc77 \
+ --hash=sha256:0cc339a807c156d840b54f8bf050ba0fc265eb81692c24bca8535b52fbd797c6 \
+ --hash=sha256:0fd73d0bbf700a30dd87e4412adf41cfa71542a533d6b390c7244bbb8a1152bb \
+ --hash=sha256:130635fea6e611a6b2026120037965ddb88b3dafd11bb64e264b101a70a76630 \
+ --hash=sha256:13873eb8aef5972fcfee614f63d47064312ad4efbfe65ade15b8a3b77f8c8659 \
+ --hash=sha256:18d800aaa2d6bff7d889df810d1b19a5fde72b1f6c0ca96e8d9f28a692fe5460 \
+ --hash=sha256:1a4050a651e1f2faf05eb028ce9f2168abbcee9e24b209f5c1f2eb96d8c569e4 \
+ --hash=sha256:1a7f1df31829c258158be01bb04eb668c4fba7df1ddf2262131a972962e651b6 \
+ --hash=sha256:1b01c0fcd6725a8d79a164ecdc4116866282479d68bb3d6d74a909bf994656c4 \
+ --hash=sha256:1b95775f6292d72cb452c33e5c0f8b8551807c29a10e3c1671fef7f61361370a \
+ --hash=sha256:1f6da814aeecbc6cb8872d6d3e85ed16e8ab1653f9557cea8658725ce212348a \
+ --hash=sha256:2095207b75a83c9e947346da9c127fb7e4fb29f41589df2643764f06b750989c \
+ --hash=sha256:22ab1b10b06d357f01092e60f5e6856a0d479ed79b0ec2166a339ea26c699be2 \
+ --hash=sha256:2319858018eedd0c0b2f950a620413c0a9d1352607be4267eb28209eca8b1e3f \
+ --hash=sha256:268d18601feb5367885c6ebf6f402c18fc25a324cee215784adafe0a1eef925f \
+ --hash=sha256:26e1d9629f3bf70d23f0d22238152aec51c837a7c9e384cb74f356fdccad7eb3 \
+ --hash=sha256:272db0c51e8b71e953c1f2ecbe63402b819680e4564be2ef285cfd4584ee8355 \
+ --hash=sha256:289f213d2a3dde2e8312c415ffecec5a01698589ec6249ec4e8fb3b47c0444ba \
+ --hash=sha256:29b0d823e3c1e7cd1093a5dc889245db693ef13ada624cd66e2262421ef38867 \
+ --hash=sha256:310266a2db1377ffae3bdf6556ab4973f4f94508a8ce37b2f6bb096a89bcefa1 \
+ --hash=sha256:3238e198429cb8909ec42951b82d6a33fe0fdfcf86371732f8f09311c5b8ac32 \
+ --hash=sha256:34266cec8c1d4e3e91fcca7efe38971d6bdda64a7944f2a46ab576da15173680 \
+ --hash=sha256:36fac804b8cfd6b935ae64f71349f833d2b6298404626d017a2c57bb942bc643 \
+ --hash=sha256:3af4e45ff455fce5511fdf2653c1ce428ef09c56fe37a83eb4d924c2d474f31e \
+ --hash=sha256:3e3201fe4d46e0d15d7ff9fafc94a605da9eb82d2c5b9837f0368acb325481f1 \
+ --hash=sha256:45b3002392948dcf578029c89f6318e1289a993a1a5ec38a4161560fab60f811 \
+ --hash=sha256:465bc1526debf53a3be92022a16ca0c38f891ea3b5c1587af4f52e44020f8a07 \
+ --hash=sha256:48c705bd0b1afb6253ed71eca9f9ba7ac7d47838e5fed1ef7891d67f21ecd4de \
+ --hash=sha256:4a4d8c2c7e73ba5967be74d7c3a5ff81fde815ee1b48d9c5c0f14de8463a847b \
+ --hash=sha256:4a85401b0c3f893cf5695c1199e8679fbf673f7f78c2f6c11d6b1850f8c7e358 \
+ --hash=sha256:4c58dc91aefb31adad500aa68054334f429b840b36dd29e34e834101044cb2ef \
+ --hash=sha256:4efbee349138a3fee7a4cc3a95abd2d499fae70dd5bff9fed9138d6f570f4283 \
+ --hash=sha256:4fb995082fe41ec410b33c48b54fb1d44abb8a6ee762c31e8c42519e8c3a30a9 \
+ --hash=sha256:5042aa1c7e2b1a24c17dab31d8770b63a5101c9abc25f832c6aef6b201e1ca4f \
+ --hash=sha256:52fe0176682a25b15370f23f5b0f1366a84771df89144fb0cd979cb72a94b5ca \
+ --hash=sha256:5332a020a60bbe32ede4bda1a62b3d56c4831d309cdf0932842c0fca8ad6aaa3 \
+ --hash=sha256:563e4568217dc907a91843f38c737be865222c0400a38cdcd0d26ce92b3db271 \
+ --hash=sha256:581b27663c6e9f4df68068f32fe6d1cd7647b31fac90237221a66f8821c342eb \
+ --hash=sha256:58a1b0ec4cbb930e69669f9771715b2c7898d3cdf064d9811f7a66afef96b544 \
+ --hash=sha256:5cc5d3a29f9ec86ce406e5ec09c241dd8dc4d30e838f74f68d728b89131a3acf \
+ --hash=sha256:63d38e9a9a10a20fb57593742e63c6b1e78dd7f6ef5472de8e0b1e4cf4f3db26 \
+ --hash=sha256:6b1ac7f1bc6c0dbf90684b77571a51a21b2463909fd916ce0ac9bfc4d566dc75 \
+ --hash=sha256:6b900073e7b8481ef1aaf4f6c1789d210a1db01a9da8789821578cfeb4c2d540 \
+ --hash=sha256:6c12d0393a903b58bc5f5a7406d6c5290acfb8284290d68547ce620c06f7d133 \
+ --hash=sha256:6e2780e33a58a93f27cc3bb74a55bae6f9a8278a1dbabdff392940d30d381671 \
+ --hash=sha256:6ebd39ee26db460cfe5ab8b71a15d1149b289139a0d3981522757d6af620887e \
+ --hash=sha256:6f8b41299b203ce8f627db670cfea82067d9638853dbeaf86dccd93878879b85 \
+ --hash=sha256:6f9549ca354a1d6d6167c458a1f1b12147726b968f02dd64b6a5801dba91ae0f \
+ --hash=sha256:6ff0145b34610e57c9fae20df4e133c8d54266447387de6fcc0bdabfe4db4569 \
+ --hash=sha256:6ff5f0ed70783dcb9562dbd20edca51c3d4d277f128223709e3da6b75986d1d4 \
+ --hash=sha256:714bf348f468532d86bed670837e7d5ddff3834dd7f5d3c08066da400c86f088 \
+ --hash=sha256:757e3f79cb865a7db94e0db5f4d0ed3284a69e39d53568f433982ea13c60cac1 \
+ --hash=sha256:7e32b83bd8c2f8b6fa726ef34e63e21c4d7eddc277d40d4ef7245ea3ed28e5b6 \
+ --hash=sha256:805b0f2618e5d4c3e28f45b731eb1a0539691ae4a2f97b4ce014de0bf96a1ff5 \
+ --hash=sha256:80eae881cfb69383303e9a4d7961a478025b89c24f38f2e69b30c516fa0d57f2 \
+ --hash=sha256:813a32f94991b9627795528053c73a57d2ce3eb98ede89f0e1c7a31095938e81 \
+ --hash=sha256:8463b34ebde3f000627e9dbd8a545f995ad49fbf7ff9dd5abc0cd507da98a603 \
+ --hash=sha256:8a59c749a73fbdbc8e63b895a3079825fa085d752e75bc0a500042cb8a801e48 \
+ --hash=sha256:8d90d10e9b6594c28f27896a68fab97fd784c43804e9fe419dab8e8dcfcf4b02 \
+ --hash=sha256:8e1e037bb57dbc549c6fe20370b763ea74bdb09413cdcf857e4f14d9e4e2fb13 \
+ --hash=sha256:931f45f84e15daafec5f82cc92e6710569e1f50933f3253d206eab4132bec678 \
+ --hash=sha256:995b52f7c260ac7023640221f27472303968753cb6fc6fce1ddfb0e9db59a398 \
+ --hash=sha256:9b4da5789d7cf576c7e81f0088c632f6ee3786d87d17f08e90e703c22ce15633 \
+ --hash=sha256:a3ed60ea9a7c352c590182c67404599e6b5a0c901e75ae4cceee9a9fd6bfa455 \
+ --hash=sha256:a4d1ecad62e83cc65b411ea0125972cf3af98821e8117129947fd1e3a113f8d2 \
+ --hash=sha256:ae9bb62a7902e2ab65782447cd3eeb753510feace4e3ea03937a85489b01b16b \
+ --hash=sha256:b2ab3aad55d75d0b8df8d8a1b5920baaec9b161112cd5e95984848b4d2cd3dfe \
+ --hash=sha256:b2cc6991f16f6d666d48e4b57318104e7b29109e32e2f6b86e9d44c4e6a27f4e \
+ --hash=sha256:b5a3f5f70967a1aa2bc47fec42a1e19d2fb38c61700e3ee62b63a4af4f4fd001 \
+ --hash=sha256:b68fb053b37c258a473ab67f4965c3b439500dc160fe364667035a6833eaf50a \
+ --hash=sha256:b6ee42112d785a913dd63ec0335435a3dddbea5040c151252db815b0095cf066 \
+ --hash=sha256:b928ab0ecaa664e8caecc529dcb8bc881b6b35bb2b74bf9a39ae25f982ee8812 \
+ --hash=sha256:b9430f65db521db7962ad951571d446171213686f96c998a54dc18ed574821e2 \
+ --hash=sha256:b9cd15cb7cf0d5cc41f649fd789aae12c56c3b83eff593f8e095c1d4555ad5c3 \
+ --hash=sha256:bb1533541c729ad422f870a780d8b4af924f9817d45b5f580390418cda72eaa2 \
+ --hash=sha256:bbf7377fbd41b7c87d47820e25b9876724963681c2a1d6f6ff2adb4db46ac174 \
+ --hash=sha256:bca180cbe84e4fba7807eb408a8655295f697928512324517e30a091ede522a8 \
+ --hash=sha256:beb2c8a34cc90fb4d862b7284eafdb322030d6a8b2ee5eb6a744f84205beedc3 \
+ --hash=sha256:bfdabac0c6d3d6a5be8c2a100a001c92c14a39bbafd5999545a675c493626e64 \
+ --hash=sha256:c0e45def4d9ce7073e2226535572442d9d6efb4047c7a5fd8960807e877ce70a \
+ --hash=sha256:c0f537e5e8152e8d9cae82804024790cb973061abd3b7ef8f66f46e2b5c7bb51 \
+ --hash=sha256:c195a69df0ab2541252ab5b1d76e3c182e5688ac2a9b708e5e6f66aaeda91e9a \
+ --hash=sha256:c271bfb832be5c5c020b4e2fcbc1e70a0b990adba6de874b0bba1184b89cdea3 \
+ --hash=sha256:c42424213c28804f8d0e20f5692106cfb57bf72e1dbc4092b8481fb2f9e4c707 \
+ --hash=sha256:c4fa57d3c31889722f64bfa785545a5e603a893b6f29ac1a41bfa830abeaefd5 \
+ --hash=sha256:cb2bb3ac0af7fdab2311b895c9eb95442b45deb14cc949b9e65545e74aa0be69 \
+ --hash=sha256:cb3e7a4fd0168e362673a980380bf4fd6ae3b1555150e60c5390b4b10d9c50c4 \
+ --hash=sha256:cbbfcd5d15056fbd1edd5e725cf3feeb47c7cbccbe205927ebab422cc229f417 \
+ --hash=sha256:cd3e55223a77d6e08d5730ebacb4930ecca5d2ce7c57e7ba10833be7e52903f1 \
+ --hash=sha256:ce8e723b4637034b76f5382a30a6b725518c332273e8d62a6c7d46e90837c947 \
+ --hash=sha256:d1e329a1866981efe0201d05a374617f6c6cf14434a501d78ab22793d1ab1fa6 \
+ --hash=sha256:d20ba5c84cf0592afb2713336f07e2b6ced082e4ae803ceada153a85613efc9f \
+ --hash=sha256:d2b095129b9a98eb46a271ee9631089529c4e40354576b4aa74e24de9d2bf2f7 \
+ --hash=sha256:d3906b5c549ff2ad2473cb711e1fc65d76715c2726a402108fbf55eab6c6b49d \
+ --hash=sha256:d484ebb7e3a3f3597b0f645fbd1b85633674ca808c1f5ba11c2caf7c66f5c8b6 \
+ --hash=sha256:db735a23ecb0f0450d2b24e0a05fb00a8a35c9db172919c4d3e023e7c7ee4c9b \
+ --hash=sha256:dbc9fd1521e573045d71b6afab7398439c5cc259e8cb9d416fe62d485c4899c6 \
+ --hash=sha256:df3867518b205be3648e2fbd522bf380c851b5c2500588047505afdd786b6669 \
+ --hash=sha256:e0acbd474d0af4afacc6e66c4273f8a19e25f8af4379fc816388095ea6b01371 \
+ --hash=sha256:eacf0f45ca3ff84c01481c60c15da9ee56711f7292f66663df0f57af61e011c2 \
+ --hash=sha256:ead1a40543a033a6732a9e1e515944979a19db3737ce77363fc0660e38554344 \
+ --hash=sha256:eae4e9c7a0785a1a715de0a74fb822ab40084c060f444f18f075d05e322aa7ef \
+ --hash=sha256:ecf7037e491c220cd73987838c1ac3958d787bb098c3be0bfaf7f04204a6162c \
+ --hash=sha256:ecfeee649184ffd800955068be9a6b579a0f33fc3c98535d685d5779cb59347f \
+ --hash=sha256:edd5aa045fa3cc57143db018dd32ce7962bd5b525d05230709015d7e570100aa \
+ --hash=sha256:f0ef48ce353f6b6a52232ba23d0983d4c2c84c84a778899404e34b4718509bf2 \
+ --hash=sha256:f1734bd6f588975ffc246211e8b96c11933344087ca280d2cbcbf35cf835d7a9 \
+ --hash=sha256:f67db0ba2bedafec15b8e5330d40da1e1c7921559fa715af021252bfef81a6f8 \
+ --hash=sha256:f6ac1414556b910a879c108d79736f77e797871f9919ed0d2c3cf8cf3ecca986 \
+ --hash=sha256:f78f7ae1c2e5aabf29583fc0d302d8081a663776f84578025662eb6f5d63a921 \
+ --hash=sha256:f9489c1d87160c126f73b004742fe8654fa1ce37ed89e9e01330a1c10aaecde4 \
+ --hash=sha256:f9ccc9884241efceb4547a92955d128574c864681f11b7ea3ecbde295fafbe8b \
+ --hash=sha256:fc1a4f9d18d32a6e0a0a0a382986a60a2126f5144dd08715be7adb8df18e8a46
+httpx==0.28.1 \
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
+idna==3.20 \
+ --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
+ --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
+kubernetes==37.0.1 \
+ --hash=sha256:a313a482361506340b5972cabaf20a7ee16ec8713e73325bf699b783a40464a9 \
+ --hash=sha256:e41db0a95615c2388680a2d556df33fd0f27a5717c9b5df5857e475c196c2f10
+multidict==7.2.1 \
+ --hash=sha256:02009b402129a73e1f3da68f48eb999afbacd29f25aef0d5052b84ff5c2786a2 \
+ --hash=sha256:02d9c04d395a36eeebe89e45f2269614a4675b8e83fddcba15bba2be903971e5 \
+ --hash=sha256:03846be88267369908fcd9a91159358a3b142d740991c96e4f696d362fce516c \
+ --hash=sha256:08ea94663d9e7c25d14d78222f7aee906c472d4837766bec62477c38f6e419dc \
+ --hash=sha256:0c454b677214fa604e9c4a38952c685dc88a62c96f5576a7ce550f213d1dc318 \
+ --hash=sha256:0c52fdd8cb8194fcb6969a417149aa1d09d6e48b69fb1b34ee1b1e4cb51ec183 \
+ --hash=sha256:0c7b4d8cc9e6ae7070b8129bdb840215552d4e2689ca765d37eb208a24919209 \
+ --hash=sha256:0d852d3d9638f852158d7de7200dd77f7ff8d77fc8f3d396032f3d847883f631 \
+ --hash=sha256:0eb7fa012ab47b13e96bffc1369642f520adcc4e3d3e170d7f1a2b39fb953bce \
+ --hash=sha256:0ec95a02f9f08246cc7f1d85ac425e4d5c233fd3f098f5be1b2cca16aa690186 \
+ --hash=sha256:0f30d6eaf3cb309b6c21096c6c2b4cf3800f195a78b1babae85f481ce851bdc2 \
+ --hash=sha256:12bbab225ce7f69df75409ab8005b11b4e9309fae863821e019e81b32e53a2a8 \
+ --hash=sha256:13268013a3db11a214ccb6920f778d175b765f69a40509610b92e43602272718 \
+ --hash=sha256:15da10cf11fd8d8716720c95b93180e94c8317405d89d08524c579ef360128ac \
+ --hash=sha256:1d369ed297e79b417766327ece16bb7e89306c588df63c364a85d90d4aeac843 \
+ --hash=sha256:1fb8c1a3480c130c3a6422d5f16f3cad6552bf08c2089dd288ba7134649183b5 \
+ --hash=sha256:2022bd3194968a027f64ed16f9779e62e71a823a9df3971a7328c1d45dfeb9ab \
+ --hash=sha256:21d86f2aa58aa428fb9909d6b8587b75eea827287e3515b8506dca86b55e2c7d \
+ --hash=sha256:223427392fdf993e198644ec40c404fd93f1cd2a61328d18842ecbf154d02c82 \
+ --hash=sha256:2529873a3bf68c372532a9f8058bcf4199e78387e92cd6a01b73334bfe6a7e3f \
+ --hash=sha256:2592fb6c335d67f1912bf72788c02fd5dab305fc3c8c7eab078a5bec1aaa0f88 \
+ --hash=sha256:25ebc4208d9098f77caeb16df895a0e56eb6c3b22870ac4e3ec43e0a9d6a33b6 \
+ --hash=sha256:27166b6ec73eac37696d66b2c6efbd44b6d04b597d655070959df54dc7c71c0b \
+ --hash=sha256:27cc258247b13008742f0e239933ad01dec5ed4e6dfc63849e30e04e1aa5bc82 \
+ --hash=sha256:295a15ccb8cfde9351dd6062b80af4e839460636b3c46813b4aa443905ded86a \
+ --hash=sha256:2b4d0c4e13d329ffee73d46128e32dd3b4aeb2f9c6ac9c8a89eb28d3b82dcea8 \
+ --hash=sha256:2bac371da2c3554a99ba126f545e79ab8e8d9426a3e41df81d00ad725c1e2cbf \
+ --hash=sha256:2c4e6a97f79699f33e6dc13bddb5631781ee79a4a94c1822116d100e7d5c136c \
+ --hash=sha256:2d4cfbbb1023f0f6795b5395c400752fb23a5a09ba756aa9003886bbc47b3e12 \
+ --hash=sha256:2e279ad5a6e9347bc9e043fdbee180a0480c8fd52618c3258e983dd93d98ed49 \
+ --hash=sha256:2f3cf710144b1859f4128051cc27ea86eee83caac5648cf2ca0c1b835261cbbe \
+ --hash=sha256:30572b61c7932e4d3c2ad1e580de7db1f35b7a35d36db38f89397ce3f2e69096 \
+ --hash=sha256:335f715f8c111654f29e4bf19086bdf0245579d2b4f4f59ca8dd9b3003b859c1 \
+ --hash=sha256:363e5db93fc3f65f3218a79fe108d217b9d264181a4e993c77552bfe275d0393 \
+ --hash=sha256:36b868758fb11cfa748c788b793994e73660abec90f83c780a678539c2c468bf \
+ --hash=sha256:37b161535e92776788957a165fcf38a80b33c1efde7b18cab287db4e4574df78 \
+ --hash=sha256:3b7542fad2706595c1b1853b46f2c3c4ae5cc5a6252b7225f294d607a29e1bdf \
+ --hash=sha256:3ca4e2e9102d0788b94894a30a5e3561fd4818a0605d8748d8fe780f890aa069 \
+ --hash=sha256:3ca60708f0ca9d994f46790c5a5c774771e2f1dcb75b12c70a5582f915d0e721 \
+ --hash=sha256:3d22ebb716ac79bf32c2a1d15250f017faef719114af4376e8dd814d6633cf0a \
+ --hash=sha256:3d6fce7cc4ef63d9a7633cbae43e737798412805a4c3ba876cdf472dc6ff9c51 \
+ --hash=sha256:4b338b45dc26ca4bac702e1278754f78613d4d0081e14e94e44fe8e261bf1750 \
+ --hash=sha256:4bebc9587cc29359e3b045518a7a09264fa882a7d165b0d143fdac8eeb7ea948 \
+ --hash=sha256:4d61be3725016ef5db3543c95f1f062d7d150ba776582d959a982af88e75fa55 \
+ --hash=sha256:4eb2650aa240b62ab0c6d87238bee5d14aa4d411c0d69ad61be53e026e0785ba \
+ --hash=sha256:4f31ddf9c00e0e7db848b0236c717acbe8a51954d0d31b4d908c538ab3f0f5bc \
+ --hash=sha256:522f7d2351743804cdf5c5a304bc45f9d55e935dd87706fae2dbeafe0fed0b49 \
+ --hash=sha256:5324411117bdb67cfaeb3d1d28fb783710fcca3d88b855696af99044d7219e4b \
+ --hash=sha256:554ee1d2e71fb5b4059c0c4ee35d8cbc8af2a3147036d65a454c53fea07ff4eb \
+ --hash=sha256:57ac987ec5d26d89bbc48c03e486f659e1367d425c465552ac2e4c4af8219def \
+ --hash=sha256:57b5d0df4c60ab8b1159687610a1c3ae069ad2380d390a79dedff5c82318e714 \
+ --hash=sha256:584ebfa26c9dcf3e465637a5a9afa2647bebb3022b50b1ff384834ba8e0380d1 \
+ --hash=sha256:5a7df927c7999d1d3196176a8d4139d3a0e1765936f5de8c768097275b2d617a \
+ --hash=sha256:5b944091cfa7da4f7a9bd2587569cd90c94b89ec4721f8e87fe62504f2432b67 \
+ --hash=sha256:5deadf03fb4a7842696d551f9f9a06dd4445bb43ca06b5f9736e4e1885561333 \
+ --hash=sha256:5efddcdbcb73f5485dca2b6624edc8ee063604b77bd3a3eb772826063965819f \
+ --hash=sha256:65e4448dadc7c94a8643bb41361e45f3a73d46e36a008a9bc03e9b525f4d7c80 \
+ --hash=sha256:66af55b07e4593790157e85f897f06983ded8a8692cb2fb048f3049c1d3fa172 \
+ --hash=sha256:678ac791b5820d9346eb13c8515fe0c9383659335e5cfc544bf6f54714ca1939 \
+ --hash=sha256:68b97b721d95b77d75e718d1b62a227fa4b3e647bd2a9fb39552d1a79ffb67c0 \
+ --hash=sha256:6966806cf917cbbeb9a8b5c581db52e19c9af0ef5fe009db54c1f5799a6e003f \
+ --hash=sha256:69befda4b1ef57027ea95dc6ed733568642f330fb3203a3e5c5ef107d41e0a1f \
+ --hash=sha256:6e22756c6e2d4f57ead0d2e6be179d8a16d661bee424b81de0c6c0c1305ec39f \
+ --hash=sha256:6efdeed2723431999cf3bcbc84c72dbe0f65d16bb1f84e15a657f5b60ff18ca6 \
+ --hash=sha256:714fe5f3eabd153b1b6911d0b5d31cfc2b808ae6b9e59a32fab17a4e6cdad5a6 \
+ --hash=sha256:7238f7135481926d52a94c18a8cca633a9e72c1cbfccd5796548298a479b0656 \
+ --hash=sha256:74d3e015c28b43b6c1a108eec324ea2efb185c359c459216cabd4cb98f7f02f6 \
+ --hash=sha256:76a4f5132b8dc29eb3c768ee9b47e132a1d24fbc2e9259854c01af9beaff1d49 \
+ --hash=sha256:7726c53b1d59eb57b009a9e8759d4f71049ebdacf30642a198a197f80cea451c \
+ --hash=sha256:781ca5dfc2f1d58e47f52216a0c1c7e1ba91ce8aa235459e20de2d402af77fb3 \
+ --hash=sha256:785ef2f3e372dabc131f228a275f97eca92ad17cfab1ab138790e330d99abe3e \
+ --hash=sha256:7cb1a6bb70edcc06ea65cb9a35654f268ee39d9fc3a52240e6ba074db4d9c99e \
+ --hash=sha256:7fb3bc8d58e1b7c1d9fa1d995e30a4ce9dbe49f4c63c755a1ffdecdfbd026ccb \
+ --hash=sha256:8148c2aaaf4b85d7872f024c8bb0c9b9797cc76b10588e3594a69cb01057472a \
+ --hash=sha256:81a5f84a76401c4330e6db4e170dddd58860b30c4e8a44a181e5c2809c7f9c9d \
+ --hash=sha256:81c6fc88ac9bf6920a0cdf22d3c383964167f12d35eb630463fc6d5ac89e460d \
+ --hash=sha256:822a7fcd33f6922b650e1b26594f09d53c5d011fe655fea7ded27b2728ce2a1b \
+ --hash=sha256:83fbd7cfaca614975efdbdcd9e2df45f3e987e819216468fb73e1db6f0365b98 \
+ --hash=sha256:846b17193f1c373d6f05ada894fadf56bce2473067caea0aab51828849894b31 \
+ --hash=sha256:8648dd5dc4d5db32b5936dc5e85e9a4d79db1135513d40d1f54ca15503750324 \
+ --hash=sha256:873d35366cc115be02a1c858f9b22a1a9f6182b99aab2171ee5276af02a153e3 \
+ --hash=sha256:8c3ff4f5c376df7126695a52af61e6084b69c9e52dcb9f4713e71ce9819e1bfd \
+ --hash=sha256:90efd90a0f9b7ae3844a4fb3ae614321832d364ae19567b7cd312315fac4ec39 \
+ --hash=sha256:9306e0c5595f91874e685be0a9eb743d48ba069c888eea051bec3c67c08b5b8d \
+ --hash=sha256:987f65f6c9321402bc7012f6c6d09ba501b88ddb1640f5b0b4a19f2caafe24a5 \
+ --hash=sha256:9a53776adc1ba923c9e345fdea7b61553c44c17a9af5eba8d8def8c5dcac4dec \
+ --hash=sha256:9c44bd978adddf39df5834c6a1efd7d075c84769a7a084168d035a2a276d9008 \
+ --hash=sha256:9c9f285f788acf2448a57c9b93d31103ac69bd03f92907e2ffbb23c4e2b42eb6 \
+ --hash=sha256:9df532dd12c59a7a4d381646fc1bf5ccb4bcdbd01c0887d07ec769fae965ea9a \
+ --hash=sha256:a0f999155c22c8929cc492949c08c6fe7ec8079a5e1bfdeb34b9a3e4991959d0 \
+ --hash=sha256:a2a322c8a6ca82a5a67ae6542e9c6a13ded9045cfd171dcccc5c213ac4fe0eab \
+ --hash=sha256:a3d28ac22fe658ad7486d1532b3a9084fbf65b428db31f47212c5b0b6da826f1 \
+ --hash=sha256:a3d9a59bcdf8531d46b56bcd512017533510c25e955c2c546efd0743bd840940 \
+ --hash=sha256:a789e3d27fc5665cc0477f10a35a89383313e4c9cf8b5e6af7ea71f039cdcc0e \
+ --hash=sha256:a7d120bdde5b213e67ed7dc80ea3adf4f014453fbc20195d619ba2ceb34084f3 \
+ --hash=sha256:a878fef2797a7c082aa47559af68299372edebc7367bd5128dff2d36e6d820e1 \
+ --hash=sha256:a92e3080d8889b0f7a80ea0c4a72181b4d4d5d3918a03136b4a459f51f5f59da \
+ --hash=sha256:a9cc121b5ee2c8b4bd4b6b526d5a26bb782f4d620463f73707691b2004b8c349 \
+ --hash=sha256:b4821bd30b38144d8ab75df56af0b07e9d71327c9cf94c0c7a1965f865bb9615 \
+ --hash=sha256:b5fb6b048931ea925b484f0c7c01b007981415a00a6c62ee74bbf8d229d39c08 \
+ --hash=sha256:b63acdf4df5a78a15223bbb510bb3e0f8234dfa807610ccb8796d0842db2c801 \
+ --hash=sha256:b81ff0f2d375988152614eaea4d5023c98ecb08f77df4bb1d96762504bb86fca \
+ --hash=sha256:b97bffa5772b7e853303de094829fcd3a9bd49fddd70534651ee4933691f6587 \
+ --hash=sha256:ba5e8d9310a794b853fb0e327d46a827b5b015576341219a591f88ed9d799c27 \
+ --hash=sha256:bc9decb36abd00ed5d123db9df70eb9a0da45d6376d86d64f4a94bfd5a1b83fe \
+ --hash=sha256:c10ac5e3e76837180656079008f71add8339b00c3c26314092bcd1e8e449049f \
+ --hash=sha256:c753b820738cddc097892fee95e7cd716fbdfc9640414c5e9ce9463ad125321b \
+ --hash=sha256:c85a0a54a19429a17ef449e967baffe7b5089cb4f6ef2894c9168a69fa024d05 \
+ --hash=sha256:cadd02ca804d6ebe799a7af19197d9730094fe2cc74b70eb4f374f4b8c1886c1 \
+ --hash=sha256:cb43bb34346155d479acdc5423afccdff79ce1fc66adfcc9de995cbec8a13acf \
+ --hash=sha256:cb7fc8820b206932ec6d8f8985aeba6edfd2932e13b6c4bf2434d6bcbbeef3e1 \
+ --hash=sha256:cba29f12cd717f1cbbe220edf005666f8df0453347217748331a0c2f7522eca4 \
+ --hash=sha256:cd69c99affd1b4debc1f9a53058ea6e3a9b34f2e733840bb45891666cd8ede0a \
+ --hash=sha256:ce0f884408938fbd72e764031d0e23a8f5e438a7bf73641089c7a6465cb84456 \
+ --hash=sha256:cecb2c778f00a65c6ca01410dd194d0e797a9eebb7c6dea0db87718719f293c7 \
+ --hash=sha256:cff7657e79224dea46f2f2e58eb99186656ca63ebe356212210c5c00e9beb814 \
+ --hash=sha256:d0381074dc21225951278d7e4a7a2241f7fb86a4d8b94154dfee1411f49cf851 \
+ --hash=sha256:d094951489bb7836342c005e790bd005e60b669aaf5c936b96dd5d68cbd2e23b \
+ --hash=sha256:d10e49b940e70e2a4e91f44098007dda0bacb78c3f8e31ba7a37ee2ce52269e6 \
+ --hash=sha256:d37773029ba034a2cc970b6eeb67dad3bff9d894393c51f6bb1c6b4f563a032c \
+ --hash=sha256:d4af0d863e72f3ea48e85d3aa1d1b426fd81e9544fd7976fc6d6753584a25b03 \
+ --hash=sha256:d5738cd929f2af8651ebb284467acb58b1ec0f2f4d42489cd0597172bc82e15b \
+ --hash=sha256:d615377a8e7bfc4f1983ede3f411c2ac2b0f7c3053f262d0437b7d8bcc4485e3 \
+ --hash=sha256:dbda2baafe08f8431fd3333be02c261023fd454607e3bde5e7dc00565f7bebcb \
+ --hash=sha256:dc6f3dcb08c4b41e64f2d6cf272b5686d9144db585b2e138a8b1664376ab3921 \
+ --hash=sha256:e0f2b33a6ae233f4da38c77ea32f90aab2cb82ba819d7b6c4f1b68cd33d871b6 \
+ --hash=sha256:e47d058c2db95010f5d2e1f6037acebdfb0e2fa4507650e1041437985c91db38 \
+ --hash=sha256:e64b86d16158954825d12799271979c6944c1fd1d03575b2f22e869c3f7d00dc \
+ --hash=sha256:e8af3e6f1bdbc31fd762b094133b478b89988eaebc63cbe82b1d9e3bbd15cede \
+ --hash=sha256:eb5c4a5b1281985271b6c29be53f2c1a9decf11e30a0e2733439c4d9d61b9ebb \
+ --hash=sha256:ec4ee5d2d624088775e61cada87bbe7629bfd936bfbd57f122a106970d8ab1ee \
+ --hash=sha256:ed6c107642a9acaa7ede32dcd428a6c87e302c5d555ecd3ec132ffa8c6155b4b \
+ --hash=sha256:edffdb3950d3a8b769ddaec3a0962fb7b76b3c96f317f6b88500455a537dded2 \
+ --hash=sha256:efe695cc2081633b02fcb95bc1b13070f6647c9ad52d42a8beeb3c2a7ea062dc \
+ --hash=sha256:f07f06db2e333b68dd52560b8b0b12b6f8bf6053cedebf9af4aecfecd2dc1b32 \
+ --hash=sha256:f15205c67d4c45250fcff5da077f0ae54102b5bc39e9ee8bab90d99e09a728c5 \
+ --hash=sha256:f2087ee23a18d1d618a135eedcafba7dfc40c6be4e8e52d7f29a1c8a2344cfcc \
+ --hash=sha256:f278f118f9fdb5faba5a6792be397fbda094d1568efa4dfe8c3595ce4ce89eea \
+ --hash=sha256:f29885fef157a602d1816439e5da964c70ebcdbbcd4bb9d2d21b7e8bb5636604 \
+ --hash=sha256:f2cc27ec6a53b1d39bab44d2b9dd21cc31e796d01b5e18a76d569721282bb264 \
+ --hash=sha256:f332b16911ffc668baec6aaf8f08a09e1af34a8ee87ccac26bd722589e264645 \
+ --hash=sha256:f5812cfcf7daf3ace5fedf6e2ef8d4b342c92fb8ea3a87c1aed2b50453271093 \
+ --hash=sha256:f62172cf387e92f4928b60e5936b02c06e3c8449ed7fb440978d6c9bcbfc85df \
+ --hash=sha256:f9c118e866164ce05498d9106a4bd01e745321403cd6f0b77a0b2587768f8c06 \
+ --hash=sha256:f9e78d98a29e969a001e953eb09167a48bbc71249e804dae5db987ee7e6531c9 \
+ --hash=sha256:fa0ecd064b0ae18d235283abb5adbc4e07304dba8f2aa7c0d61f28bebcc70d4c \
+ --hash=sha256:fa949c81e3865c10f981dea72fd8d418b59edd13a047a0aee89ae64790113d53 \
+ --hash=sha256:fba33e264cdd2ec43b762b9960d1648478eecfc1ce6e8159e7573510f0412c0a \
+ --hash=sha256:fc45ff19520bc5321dcd9c6dd7df6f35ddd894eed35e5be65d1828a9c3f5e7fb \
+ --hash=sha256:fcfebcb537f419e415b6e2bfc77ddbd33abe3c036f0105410a25a14741df9c31 \
+ --hash=sha256:fd24c4425fdb8e822601335602375c6d0cb020f7a368e1d5842eb7925a9c8748
+oauthlib==4.0.0 \
+ --hash=sha256:624c28c13a0a59cabf9747dfa52af63be3e512a7f2714df16e91b5b3a145e6cd \
+ --hash=sha256:efb274799819440f95b4ab3b818869f1ce9ae26c5beacba0201d1a1b76b54f86
+propcache==0.5.4 \
+ --hash=sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5 \
+ --hash=sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432 \
+ --hash=sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72 \
+ --hash=sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97 \
+ --hash=sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf \
+ --hash=sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983 \
+ --hash=sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77 \
+ --hash=sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1 \
+ --hash=sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c \
+ --hash=sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284 \
+ --hash=sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827 \
+ --hash=sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b \
+ --hash=sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8 \
+ --hash=sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745 \
+ --hash=sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae \
+ --hash=sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb \
+ --hash=sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320 \
+ --hash=sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d \
+ --hash=sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b \
+ --hash=sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec \
+ --hash=sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4 \
+ --hash=sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c \
+ --hash=sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc \
+ --hash=sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a \
+ --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b \
+ --hash=sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9 \
+ --hash=sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b \
+ --hash=sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea \
+ --hash=sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51 \
+ --hash=sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831 \
+ --hash=sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba \
+ --hash=sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad \
+ --hash=sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56 \
+ --hash=sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f \
+ --hash=sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25 \
+ --hash=sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2 \
+ --hash=sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d \
+ --hash=sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d \
+ --hash=sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d \
+ --hash=sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd \
+ --hash=sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a \
+ --hash=sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c \
+ --hash=sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111 \
+ --hash=sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e \
+ --hash=sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac \
+ --hash=sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c \
+ --hash=sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760 \
+ --hash=sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57 \
+ --hash=sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7 \
+ --hash=sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993 \
+ --hash=sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3 \
+ --hash=sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3 \
+ --hash=sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757 \
+ --hash=sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc \
+ --hash=sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51 \
+ --hash=sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0 \
+ --hash=sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838 \
+ --hash=sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826 \
+ --hash=sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8 \
+ --hash=sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589 \
+ --hash=sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef \
+ --hash=sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8 \
+ --hash=sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468 \
+ --hash=sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177 \
+ --hash=sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab \
+ --hash=sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487 \
+ --hash=sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5 \
+ --hash=sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788 \
+ --hash=sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece \
+ --hash=sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792 \
+ --hash=sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62 \
+ --hash=sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d \
+ --hash=sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f \
+ --hash=sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47 \
+ --hash=sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944 \
+ --hash=sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21 \
+ --hash=sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5 \
+ --hash=sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737 \
+ --hash=sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e \
+ --hash=sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12 \
+ --hash=sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161 \
+ --hash=sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e \
+ --hash=sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9 \
+ --hash=sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9 \
+ --hash=sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb \
+ --hash=sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526 \
+ --hash=sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d \
+ --hash=sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc \
+ --hash=sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef \
+ --hash=sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9 \
+ --hash=sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b \
+ --hash=sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370 \
+ --hash=sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162 \
+ --hash=sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546 \
+ --hash=sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1 \
+ --hash=sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f \
+ --hash=sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f \
+ --hash=sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38 \
+ --hash=sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894 \
+ --hash=sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429 \
+ --hash=sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8 \
+ --hash=sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a \
+ --hash=sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680 \
+ --hash=sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d \
+ --hash=sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c \
+ --hash=sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574 \
+ --hash=sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8 \
+ --hash=sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867 \
+ --hash=sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6 \
+ --hash=sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30 \
+ --hash=sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4 \
+ --hash=sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9 \
+ --hash=sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f \
+ --hash=sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937 \
+ --hash=sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3 \
+ --hash=sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031 \
+ --hash=sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39 \
+ --hash=sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2 \
+ --hash=sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e \
+ --hash=sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9 \
+ --hash=sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c \
+ --hash=sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e \
+ --hash=sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8 \
+ --hash=sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36 \
+ --hash=sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0 \
+ --hash=sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565 \
+ --hash=sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c \
+ --hash=sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858 \
+ --hash=sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5 \
+ --hash=sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97 \
+ --hash=sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123 \
+ --hash=sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9 \
+ --hash=sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874 \
+ --hash=sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68 \
+ --hash=sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2 \
+ --hash=sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572 \
+ --hash=sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a \
+ --hash=sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558
+pydantic==2.14.2 \
+ --hash=sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118 \
+ --hash=sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d
+pydantic-core==2.50.2 \
+ --hash=sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179 \
+ --hash=sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41 \
+ --hash=sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057 \
+ --hash=sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d \
+ --hash=sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d \
+ --hash=sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2 \
+ --hash=sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7 \
+ --hash=sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99 \
+ --hash=sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd \
+ --hash=sha256:1a5e859f5a8a6d834b0a13acaa0eaddf1a9b4b015fcda13bab7b471e18299d32 \
+ --hash=sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910 \
+ --hash=sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775 \
+ --hash=sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9 \
+ --hash=sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7 \
+ --hash=sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97 \
+ --hash=sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72 \
+ --hash=sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f \
+ --hash=sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40 \
+ --hash=sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58 \
+ --hash=sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d \
+ --hash=sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b \
+ --hash=sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc \
+ --hash=sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376 \
+ --hash=sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b \
+ --hash=sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1 \
+ --hash=sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe \
+ --hash=sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a \
+ --hash=sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756 \
+ --hash=sha256:3939538e640c94ab779a05f2d45dafedd01d65d93e1290797f3ac5b59fcb6455 \
+ --hash=sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a \
+ --hash=sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80 \
+ --hash=sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f \
+ --hash=sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d \
+ --hash=sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6 \
+ --hash=sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca \
+ --hash=sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a \
+ --hash=sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7 \
+ --hash=sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb \
+ --hash=sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3 \
+ --hash=sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af \
+ --hash=sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c \
+ --hash=sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986 \
+ --hash=sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4 \
+ --hash=sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04 \
+ --hash=sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9 \
+ --hash=sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f \
+ --hash=sha256:5c12a5a080570a91bbcbf6d10db008b80e3f5dea015ef2f0137bbec70f24f3f3 \
+ --hash=sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4 \
+ --hash=sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead \
+ --hash=sha256:6258754c473b3045a7aa30b833a8a331a1ab36c6147cb230789861d80bb26f83 \
+ --hash=sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a \
+ --hash=sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef \
+ --hash=sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b \
+ --hash=sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3 \
+ --hash=sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798 \
+ --hash=sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234 \
+ --hash=sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02 \
+ --hash=sha256:701bc55d08c22f981973ca4f6fdf4ea102ee1ee3a21b818b66fdb161ea711a4a \
+ --hash=sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11 \
+ --hash=sha256:71615edeaf1ae70f5f3f558426072eede0168b7e5190fd9633878e29a0b85732 \
+ --hash=sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92 \
+ --hash=sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e \
+ --hash=sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6 \
+ --hash=sha256:7f9821dfc0c473c96ad55ce4189c7cb4f9c94e7d4f563afb874173243dec1162 \
+ --hash=sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108 \
+ --hash=sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432 \
+ --hash=sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8 \
+ --hash=sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601 \
+ --hash=sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780 \
+ --hash=sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544 \
+ --hash=sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46 \
+ --hash=sha256:89213368eef163350af37127939ace099010e8102757905c3e329f1175950ed8 \
+ --hash=sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2 \
+ --hash=sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02 \
+ --hash=sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc \
+ --hash=sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d \
+ --hash=sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b \
+ --hash=sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d \
+ --hash=sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd \
+ --hash=sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437 \
+ --hash=sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853 \
+ --hash=sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a \
+ --hash=sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f \
+ --hash=sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d \
+ --hash=sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960 \
+ --hash=sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3 \
+ --hash=sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977 \
+ --hash=sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3 \
+ --hash=sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324 \
+ --hash=sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b \
+ --hash=sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c \
+ --hash=sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0 \
+ --hash=sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5 \
+ --hash=sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee \
+ --hash=sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63 \
+ --hash=sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636 \
+ --hash=sha256:a786cf782ebb2e45a43c7118d75d452aac982b79577dfe804d8dbf2878f0b589 \
+ --hash=sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c \
+ --hash=sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed \
+ --hash=sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf \
+ --hash=sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd \
+ --hash=sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb \
+ --hash=sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f \
+ --hash=sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51 \
+ --hash=sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285 \
+ --hash=sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314 \
+ --hash=sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe \
+ --hash=sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d \
+ --hash=sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf \
+ --hash=sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31 \
+ --hash=sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371 \
+ --hash=sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71 \
+ --hash=sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e \
+ --hash=sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f \
+ --hash=sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d \
+ --hash=sha256:bf7f99a0e6616cea754e2334d129091f3260d011369d9b8c658522bfb2427619 \
+ --hash=sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8 \
+ --hash=sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b \
+ --hash=sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0 \
+ --hash=sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6 \
+ --hash=sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b \
+ --hash=sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f \
+ --hash=sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a \
+ --hash=sha256:d71a9a9352acbcb5c73d39dd003383599428067b6eda5cf7c5d335a7a0da37e1 \
+ --hash=sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773 \
+ --hash=sha256:d93c066a38f83376b5bc020e563666784d1b680a70d6ceb625fdb541af7a312e \
+ --hash=sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b \
+ --hash=sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738 \
+ --hash=sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45 \
+ --hash=sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680 \
+ --hash=sha256:ea71b94a2eb5d2fc102926321f56d2ee8eddfad2673eadb1d23e9f287a492715 \
+ --hash=sha256:eec187d2876abb6a74203efbe2aa796455c1809334acf4e739dde35da8cfb69f \
+ --hash=sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329 \
+ --hash=sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9 \
+ --hash=sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db \
+ --hash=sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23 \
+ --hash=sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0 \
+ --hash=sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848 \
+ --hash=sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd \
+ --hash=sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c \
+ --hash=sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67
+python-dateutil==3.0.0 \
+ --hash=sha256:3c79630202e4152c6add678446984236efe4f57e84d8e892aab0637bab82e9fd \
+ --hash=sha256:7ebcb3c29a92f3ceafe79a7a48a5c45677e9e004bbeec5742ac0bbb00560b7a2
+python-dotenv==1.2.4 \
+ --hash=sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc \
+ --hash=sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0
+python-multipart==0.0.32 \
+ --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \
+ --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23
+pyyaml==6.0.3 \
+ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
+ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
+ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
+ --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
+ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
+ --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
+ --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
+ --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
+ --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
+ --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
+ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
+ --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
+ --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
+ --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
+ --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
+ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
+ --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
+ --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
+ --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
+ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
+ --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
+ --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
+ --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
+ --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
+ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
+ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
+ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
+ --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
+ --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
+ --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
+ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
+ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
+ --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
+ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
+ --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
+ --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
+ --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
+ --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
+ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
+ --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
+ --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
+ --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
+ --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
+ --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
+ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
+ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
+ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
+ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
+ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
+ --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
+ --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
+ --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
+ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
+ --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
+ --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
+ --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
+ --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
+ --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
+ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
+ --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
+ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
+ --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
+ --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
+ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
+ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
+ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
+ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
+ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
+ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
+ --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
+ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
+ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
+ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
+requests==2.34.2 \
+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
+requests-oauthlib==2.0.0 \
+ --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \
+ --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9
+six==1.17.0 \
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+starlette==1.10.0 \
+ --hash=sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4 \
+ --hash=sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b
+typing-extensions==4.16.0 \
+ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
+ --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
+typing-inspection==0.4.4 \
+ --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
+ --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
+tzdata==2026.6 \
+ --hash=sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec \
+ --hash=sha256:85d2cd9c09f81f0bb29db27759fc83af4f1e620efb814f8c5bf8b4cc4feece6d
+urllib3==2.8.0 \
+ --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \
+ --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63
+uvicorn==0.54.0 \
+ --hash=sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf \
+ --hash=sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620
+uvloop==0.23.0 \
+ --hash=sha256:0305871ac712f54b62af73f943dbf21ae3ce80a44bc0f0151424484affa85645 \
+ --hash=sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208 \
+ --hash=sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4 \
+ --hash=sha256:0efdd55bddbd36bb2fcb842d64c0d5f6407c6958c68088cc25df8c09edc5b5fd \
+ --hash=sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc \
+ --hash=sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5 \
+ --hash=sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb \
+ --hash=sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f \
+ --hash=sha256:24c58ae4a83e93a04c504bcc678125e36a0bfc44af928ad69444880c60f187a5 \
+ --hash=sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27 \
+ --hash=sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65 \
+ --hash=sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330 \
+ --hash=sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55 \
+ --hash=sha256:42feced24b9b44b856c633eafb5cc5dec354972da55ce77598db6844c054bc7c \
+ --hash=sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63 \
+ --hash=sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8 \
+ --hash=sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f \
+ --hash=sha256:4bb7f5d0b62b5afaaaea2b7b60d508921c24b0fe39c22c1438bec1811ffe10ec \
+ --hash=sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027 \
+ --hash=sha256:514698d3683189031dcbfdc31e87115992e5ce9e1b19fe5359941323f2df800c \
+ --hash=sha256:53c2c5d7e2024e46776c2d90e6c637d01102126b61aaf5faa5edaf05f8b5722a \
+ --hash=sha256:55d6f4135d914305929fe9e9c44d8b5383a9b3fa1bee3bfcf60ee97e01af07ea \
+ --hash=sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254 \
+ --hash=sha256:5a3e0f56ec19bfd9ad1605572878dd6ff7f01b325f4fc154812ae70d615c3aff \
+ --hash=sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d \
+ --hash=sha256:60ec798c40a1810d282ee046f61ecac1c5675cb898763d9f08d97d53a5e00a81 \
+ --hash=sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e \
+ --hash=sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405 \
+ --hash=sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f \
+ --hash=sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507 \
+ --hash=sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208 \
+ --hash=sha256:80cac5cb90ed7b9b72a217a1d6982b15b829cdbd0ee6bc19b93e3a9e47fb0ac9 \
+ --hash=sha256:8af88fe5c7dd68fe1fec6dea8155caa1a47155d219a750ff34049541cf536a5e \
+ --hash=sha256:8fcd721113260ffb5e38bf14a8725b17d431f34209f7d1c7005b667946e630b3 \
+ --hash=sha256:93087a845cdfb35753e539354ac9551bdd2ff528c202a98df0ae46e852bcf021 \
+ --hash=sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3 \
+ --hash=sha256:9bf08e4b6362dd1c08623bbfa2d061e8bac0f1da8fc2007062cfe1dc360a49fa \
+ --hash=sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d \
+ --hash=sha256:ab17b3a8aa754be0de0e397f7b95f13b14e56f077a4c6ae295e3d4afd199b325 \
+ --hash=sha256:b0d106d9314546d69b3df1b5352639aa628530ec3ecef8a98a21942d2a2a64f5 \
+ --hash=sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd \
+ --hash=sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49 \
+ --hash=sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac \
+ --hash=sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476 \
+ --hash=sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53 \
+ --hash=sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a \
+ --hash=sha256:ce17bc317d089f361b33521654c13e30eacfd3d2034fd34e613ca9c51c969686 \
+ --hash=sha256:d918d6f304a309222a784bbd140b85ec5594d97e4dc0e79f590549d28970663a \
+ --hash=sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848 \
+ --hash=sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5 \
+ --hash=sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb \
+ --hash=sha256:e49eba8f1e28e7c03648b7a476e1ba05309e087ccdea859fc6dd659564aa8d7e \
+ --hash=sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d \
+ --hash=sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410 \
+ --hash=sha256:f50b580fad005a092ed87c5a3a4683459b21d1620497d6a5bccad203bee4c071 \
+ --hash=sha256:f5576e8ae1723ece60d8f93c6710abf784714e99388bcf023ba9ca800bc587f6 \
+ --hash=sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2 \
+ --hash=sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda \
+ --hash=sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f \
+ --hash=sha256:fefea5cf8cdda9053b962ca8a90216fb0b1d40907dcb6819382b42e483e6e9f6 \
+ --hash=sha256:ff7144d8167e513fe39fbb46bffb4f6f192dfb1f4b0b4e9102e1fd4f212e4747
+watchfiles==1.3.0 \
+ --hash=sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441 \
+ --hash=sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95 \
+ --hash=sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4 \
+ --hash=sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61 \
+ --hash=sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977 \
+ --hash=sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509 \
+ --hash=sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1 \
+ --hash=sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5 \
+ --hash=sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661 \
+ --hash=sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef \
+ --hash=sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f \
+ --hash=sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c \
+ --hash=sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30 \
+ --hash=sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80 \
+ --hash=sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e \
+ --hash=sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207 \
+ --hash=sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0 \
+ --hash=sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef \
+ --hash=sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1 \
+ --hash=sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c \
+ --hash=sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29 \
+ --hash=sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7 \
+ --hash=sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935 \
+ --hash=sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3 \
+ --hash=sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39 \
+ --hash=sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc \
+ --hash=sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26 \
+ --hash=sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a \
+ --hash=sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6 \
+ --hash=sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694 \
+ --hash=sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca \
+ --hash=sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a \
+ --hash=sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3
+websocket-client==1.9.2 \
+ --hash=sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0 \
+ --hash=sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce
+websockets==17.3 \
+ --hash=sha256:018fab26140ce221c3853454a608e4e7004f4f10641cb97819dcd5b917bb2e99 \
+ --hash=sha256:07246d4f0293ef0b87eff3d7dd88c1f03dd9f678fa39f6de87b2af8504e7e0de \
+ --hash=sha256:078d1d41f45ee34eed5a4f3bbe7c588caecbdd6e12dfca73e8c1b0e803773793 \
+ --hash=sha256:0824e6ecee838cc28e6481062574c7fde49f9a2cedfacd36f0e91cb4b37fd6fe \
+ --hash=sha256:09f007b2150eff015b2109f006e2ac2012d376f41486defa88ae78a776b00cff \
+ --hash=sha256:0a0bcf66289b9d017c175562c35c5304b5cc3ed56aded994ae30b0a9654ba405 \
+ --hash=sha256:0a657b51e4a5f4ac577c11e706ff9a884ea5aaf3e2666bae283a251c63e66acb \
+ --hash=sha256:0c63ad657695e8f1a48624754cb6b464118ac50d4c1d477c26461c98390f0a62 \
+ --hash=sha256:0d20d6ea5aabbeba2e054d33e405192ef75dc12947bbd00ec97d23b14969d325 \
+ --hash=sha256:0e65260a1b1d7b81651f47343d836de70b88b02358ae3658efa6382104ec37b1 \
+ --hash=sha256:0f78bb2e82fe88912c9d7b25c98e57a9b08655579af89f164fd4c0657a075d62 \
+ --hash=sha256:10b24e836ee0233ca078e1244f6ba40343a22127a9a736f46a7e7b18de2cc711 \
+ --hash=sha256:13cffaedde6e06183860fc92d88fe1d190abda73837fe4c14f5d4803c472f5dd \
+ --hash=sha256:13eace1b315345ebac5b27821f5dcce24e5cf9976ab18f79e00a59bdd67e5d19 \
+ --hash=sha256:15decc0fe293d9e3f924bd483f3d76f3d0ff26845112a470e3f8842fec2c82a1 \
+ --hash=sha256:1c7dd93a1c9f89297471493674b1fc1d9467cafd45374f2172213046bdb3a329 \
+ --hash=sha256:1dae5cd6c92d4440b15f20aa2987baa73c47ea14baa0fec8536a542e82e0ab33 \
+ --hash=sha256:1db2604e6aedb7a2cb2f6e0575d6c9a852ccac8239703a930ac83b63309177a5 \
+ --hash=sha256:2129c9a5fc05a230ce088745a790af073e582b584badc7b05db067ee326f56d7 \
+ --hash=sha256:23046098ebf844753cfd211033624a8247520e5bf2194c24cb1a8c222917acff \
+ --hash=sha256:23cb6f6d64e3a389bb8ba150f947f9a6c8c0aca383e814ca3c5cffab738d5646 \
+ --hash=sha256:26838e2d95d7004c52e8c7b6b51676c872093a3b55a497824f899116a15a8a55 \
+ --hash=sha256:2789d203babb6ceb2770aadc2394c1f99263ec37fba57a93064f9a8ad899e8ff \
+ --hash=sha256:28d1935c16782a1e3650c9baa79069b016fff739e292e4cb14bbb830d6930491 \
+ --hash=sha256:29b01520c39ef2f23c88b002f11e6ef4165675f6c92f5f3578fd587d69b5a5fc \
+ --hash=sha256:2a377c7132b1e69be7a50d7434d7f26f69e93c70166ebb6d8f8cabf802ac4506 \
+ --hash=sha256:2b06213c8e1ce93b18d0476d7a5e647436c8f23d099888c9065e930e80f4bed5 \
+ --hash=sha256:2c460aeb12c2bd1c204944d9e9199595bf9e505eb542018d304bf9e63cb60cad \
+ --hash=sha256:2d08ca4bf5a9cdb6fd81b86b51bd5cfb10527d3a1199888f2b02cc6c6a3d5f80 \
+ --hash=sha256:2d875224bac1a05a20acade268a7a0e3498e7f8cf890acb7c2b23bacd2bd3659 \
+ --hash=sha256:2e99440ef446c7833690c1754e23b0188d440f3cb89812b0764c697a7880cb33 \
+ --hash=sha256:2fa79b9a48ae59744316c71a34cb9f76f30c93e117338522c5135cb7eac10351 \
+ --hash=sha256:32029441af953e980b9940f042b337fe4ad9a97fa8b17a4ed878ec67b5b89ef4 \
+ --hash=sha256:33221e49be79d7bfe6d12bc52ef9458b256ab74fa8ac5660acfa3065de1c7870 \
+ --hash=sha256:34c78052277053bcb09810ac8e9ba4029f9e3e277833fc18ec1c928a6246ac91 \
+ --hash=sha256:35381b575f70a08d4241e73c0cde4672bb9b2a9aae8767ca6b7fb61305a052c8 \
+ --hash=sha256:35fe35d20baa661cef4a12ee7bc42eaf2376d43ef9816ab29dd47ca957f7a73a \
+ --hash=sha256:39f532fa0d9026de4b9315a175998646112776baad54d55348435c11b05d034e \
+ --hash=sha256:3c4f631dc8d00e470cde3ec0a35d4b58a817bea592411956f1cf4099dbb306b3 \
+ --hash=sha256:3d78d93846eddc990551c1c31914d90d71957ca3af468969826f7ed0d7d1d51c \
+ --hash=sha256:411aa0fb234f42a1444dea1e177890ddfc577b36ffdfafec25cb0d2c818e1af8 \
+ --hash=sha256:41665c7ea01d3ae497a0c61fd22d3424230d18782a5fceb79f04db9ccb2042c9 \
+ --hash=sha256:4170556762c6fd2913d99dfdd8dbb4c927584899df7facb1034627e355263c45 \
+ --hash=sha256:43f270a5f21e12db08c73d83849a877c02ab7b7d0fb6e2718b5101d9c810212f \
+ --hash=sha256:443fa28652682d2a3ec3eca49af1b87cb2523561257683e62e03a98967b22138 \
+ --hash=sha256:45cc1ccb12ced6d242335159e8221733c8696da5ad0b6ceef6debb0f5ce89110 \
+ --hash=sha256:46ae05aa1aecd47d4d7de0f05ce2d1d2eb418f8de614964d754cc3b9a409c400 \
+ --hash=sha256:49fb5b4960ebe1bc557b435da8687deef1302a08f030a33c5606fa6fb11b773b \
+ --hash=sha256:4a9133dbaedc6f1af358127275676171d9a1bb3e61f47ee62b2ff9acb78af3d0 \
+ --hash=sha256:4aa85b34c21cf97b3470793d0bf0763d32bfd1e67b83067a0a3ba8dc3474dacf \
+ --hash=sha256:4d5f2492cb2192aab2d42e57f2caa2159b4fd20fde06f859ec13354f524b176c \
+ --hash=sha256:4e7b5d23ef144b32645d97bd99f382c4394c443395f45fe1576786838d26e308 \
+ --hash=sha256:4f7431e71072962d3ed1768038200aa4f1e76d9c2d7bedd554d57a55faaf6644 \
+ --hash=sha256:508775efa11d26aec186d10154359f52276e2baf9ca201b52356330883276dfd \
+ --hash=sha256:5142798871a248b422f353e0684d51dfa119de3403a6fa0904b7cb806d9790ce \
+ --hash=sha256:52a2ca6f401fb49a8d5d9e0e48ade5f58f1bdda33132907b0e4e3b702628fa4e \
+ --hash=sha256:53209dffd91b0aefed9ea630fa6ad5c2944d6210ba3a8a3a8664dc1575289b14 \
+ --hash=sha256:534ed583fd4323de45d196535a444cebd1af2105e6e4e0cdcbf8cea266a8747a \
+ --hash=sha256:54fcd24944e6d127d198c28dc363967b7131ff0c97833962c2ef2048018de02d \
+ --hash=sha256:55494475206113dbaf731dc8c5ffd5e1094175b753b7bbaad09fe3d7df61165c \
+ --hash=sha256:5670a762c7ae388bf8aa04199fd436180a4d28318dca9c1eae7b38604676dbfd \
+ --hash=sha256:56a76556ab04ab54a675acce2cac8b4e74ea2cec558a7c9ed37e0367b04b5d50 \
+ --hash=sha256:56d43c311f3519fb1057b6114dc24e9060bc3fb6aedf4aa11160484154b66b63 \
+ --hash=sha256:5868a02c4f6d698ddd19b4c9742748c03dec045289915806845189d96bc3d314 \
+ --hash=sha256:58e5d1e935a90878a935c7d2737a25c71cf4f1c13b6e6bef454ab722c4f338bb \
+ --hash=sha256:5ad409b7ea102339041281117cf2dadfe71edbb653dc48037cc55d516fb7f448 \
+ --hash=sha256:5af7070d80d278832cbca9c3c3dd44b30a26dcfa3ae1e0f6f19ff34edc27b689 \
+ --hash=sha256:5d0c871efc696ef5eb8514df64b86ab28736db753ebb42a2101f3b562f4849c0 \
+ --hash=sha256:5e0c0ed37472c4729f3001066510a656486f08f149dda1ebfd3dfe4450c9756e \
+ --hash=sha256:5e1c174b366565e101f903f9d1cc96d5508f805759dd37d012743f1c73ec0d7b \
+ --hash=sha256:62059d739af7fc035ed6e63c5390118a60a5e9aa5a5541cd0486a83e741842c7 \
+ --hash=sha256:623d4b44f64efdf235b1e8dce266057073d93cde6197e7d7684f20c297f67f30 \
+ --hash=sha256:628efe13630ac3397e7d1278b6be6f22f37a86d638a324334b4cd43c1daf9928 \
+ --hash=sha256:6342f79d6f50e680c8aa59e0615d745e867e51ffd59c77093be60b7c433067af \
+ --hash=sha256:63b87c93e28957e6dc2bb92d31727a863c9bf4a4daa080f0995fdcb0242c9166 \
+ --hash=sha256:65e937576141ac50fac32fa9736dbb3e0e4a7769f91be004011a09216c6ab05f \
+ --hash=sha256:66027e401c70d0cf55021b91b8a7ec2cd5810e20602f945b4fe6ae8b37ed232b \
+ --hash=sha256:6644eecd99c4e8cdf72e646e17b391bb70b054f44becde37dcdc48cbec7e1527 \
+ --hash=sha256:6705af9d1005127302c04d9f1533ba56e289336c465923ef878430e99295a0b9 \
+ --hash=sha256:6a8248b1ba8861e48e4c7ced3895a1264d2a3b617de72ec94172ce6e64bf16c7 \
+ --hash=sha256:6b14914e67044724c31a9360c7b89e9d070117c630746eefdc1620d967545acd \
+ --hash=sha256:6b7b540bb321f7f0a7b8f5d107f941bb361e3a154c0b594dae0394997b91b743 \
+ --hash=sha256:71250e010eede33c1d638e2c7a98288520d67f782fd5eceecaeca3129233a09a \
+ --hash=sha256:722ab747ff5dc28070b1a8c020dba6466f13f2ab56bc251ad301fefa2454aa1e \
+ --hash=sha256:724224d4c457207271239114bf43afafc3eca4a1b4661ccdd428d11869a03bbd \
+ --hash=sha256:729989759feb81ff6493879a0d60a0bd7d2eee41001bbec0381a080318439382 \
+ --hash=sha256:740bba4223fa5216d4d6c5dcd29e697f37229b8a61b69e1a50527b0ea993ab2c \
+ --hash=sha256:7bdc658c9474864893e0b6b94f1390bf6a912952df1870dd93bb7d1a902b5628 \
+ --hash=sha256:7d53fc4b90ef6e11de639dc18744d9be8cbd70ccb5ce80027aaffe7b1ab0215a \
+ --hash=sha256:7fa69212ea60ea24e2d8aff23fe102b1f87f1b27a320f92d2e7b3468997a496a \
+ --hash=sha256:80326bc571164bbbb6bead2e35d0c943b23b24fde629032197c58151529d24d8 \
+ --hash=sha256:80578e91764bfca297ddca322c00a3136d993b829e9839e7fa378a19c1fcc25d \
+ --hash=sha256:80a6e37e235780f189558ab804f3cbc01c18f5c578995739b49613d544d34ebf \
+ --hash=sha256:8135a549e27e9941a70e2d2156676ef2ffeff4b06e9d564dbaaae963482afdef \
+ --hash=sha256:82e60c00cfb8493a5ba27cdf9b3695fdd153f666f73e11f18968f78ead1d8705 \
+ --hash=sha256:856bbb24f2ee6436af7609267e73309e4bb0a0f9982f79f1c2e01019279c6671 \
+ --hash=sha256:85d6daa3a7ca404570ae130babbeb347ef9d3faca5300fb13cfa4bb48c3d30d1 \
+ --hash=sha256:8879b6a4040ee925c638548238ab7fe612cf5fa8752fe847346da0da8be81c3f \
+ --hash=sha256:8b3bede7d583bc9f47d8693a29984882e59077db1a5238ad977fc70ae8169741 \
+ --hash=sha256:8dbb3862aa649de95660b6337d97101977f160b5b354d07b82fe309874d2c8bc \
+ --hash=sha256:8f244dfee05ea29b8064701880865eb1684f3a704a5665bd10b64694444dbbd4 \
+ --hash=sha256:8f7469408740cf06a79b01605f2ae0b2acc8839c03fcbbf05da49baee12d3302 \
+ --hash=sha256:90cc9973c51811918247a78d9ab2f54871296e4e37e69a98aba4d3c9acedbf62 \
+ --hash=sha256:90d8c4fa5ba293cf92de351fbc292cdaadb1a1d58d031a1a97ff9b8fb6250ec7 \
+ --hash=sha256:94c02b0702656f083c098bdbd93bce22a7aa7a2b34ba56d2472efb67dab7cf58 \
+ --hash=sha256:9662a9fe457820c60365b3209e03cbd8e4a817195d55de65a80fa680b4bea136 \
+ --hash=sha256:997617dd7856c5dad742bf6893b872319156dd43f47587541a59e4a852d52bbc \
+ --hash=sha256:99c5637b74e2805ee4de9e2a43c8b45f678e50abb1c2da9880f0372f6498ba2f \
+ --hash=sha256:9a215749c9a88ef2842e880215193e56ae60b856773c99702b078236bc6d4adb \
+ --hash=sha256:9bde5d82d7ac4859e0cb3a35e148f537a8d4e56aa43f42582b5f263982f095ae \
+ --hash=sha256:a004c1fe5a72c496f28c6c1d8c2dc5cfe3278d99478e18f8b838c70a912b366f \
+ --hash=sha256:a0e2e7c2c1a4f25db7b6596738d810c10732e77f852247300a1bc49e364d6dc9 \
+ --hash=sha256:a22c4956a4d858200dba83f0691bb692d77b0eeddfca80157820d20b85dea4f1 \
+ --hash=sha256:a7f391838e8adcf1f2fe71561d9e5a9f74ea7ffa5052eb2db329aac677383136 \
+ --hash=sha256:a9eacb382f400bb5865e7e6d24bbe709a84510291e707f169dd4a24ee4a5ac6a \
+ --hash=sha256:ace8dd1efa5f44c482d091aa38322e20e3ffe5adcdba6b67ad9f4e7da58f3f68 \
+ --hash=sha256:ade76f8c84ade325bbed96806357fffd353fcc29a5166d6dc6d39381f6d9f76c \
+ --hash=sha256:af37a1e6b6bd506b597d5fb4cf111fb527d1b2223608a5d441d4738827cfd166 \
+ --hash=sha256:af7c474f0d8f4d2a196370e85033873c7b063a0b5da9a4f143aa023e3bd25407 \
+ --hash=sha256:b225c656efa2dedecc81651d2290170af25887b7de4f38c5efc7c5ae2b9442ab \
+ --hash=sha256:b459d29c8b82a420a98fa0ce8e9cf787ce0062d98cc90604ef9574fb3cc26ff8 \
+ --hash=sha256:bc6f87d03e3691154673aaa8f08991723d6e3394c83ef2875c7a9049eac9595c \
+ --hash=sha256:bc8fd230ef013951900c1fda100eebe4a4b1b407dde2a44186995042dda2c628 \
+ --hash=sha256:bd645b5d2f810a181c44f4a71135e7c365b44f4be9ed3d1892635be13a857553 \
+ --hash=sha256:be7be57097a65a8fab3d0f0a10d12931deb626a7d8082eb732b1ae41f3a202e3 \
+ --hash=sha256:c15a165673bb3fb57facbf040edf5c0556d20ccd2e0ccc5f23ebb69fb715fdc1 \
+ --hash=sha256:c4b3f2c7d84f303da873b634d0beef83b5e1137294c3de4e23102841dac0cf4a \
+ --hash=sha256:c534c9524a2acd0b97b7022074bec3784ac273c683cf9535087e437672ea8531 \
+ --hash=sha256:c95d25b5382ee0e6191ecafdeabefdc1f7bff8dfee29239b9b79e760ef3b04bd \
+ --hash=sha256:c9d06edfd974e918b40e0662a7e76dac13125529ee5ec9fbe42dc0d6faa209c3 \
+ --hash=sha256:cdb8e4b0f6a41b857c74c03116e8ffbef59ef8e533e934b532843178990d01d5 \
+ --hash=sha256:ce39283ef08b0f28478e920d4d89a1fc169176e568e8a6a821332673145522eb \
+ --hash=sha256:cfef7f7aa434a1e632e0ebda14b39f4f1d7975e0fce5c77ea7b43156fabf9bd8 \
+ --hash=sha256:d2605a3b2af8d66f8b75312d223642c065ec773446c04566d9030ad5e2db13c9 \
+ --hash=sha256:d338813a440bf6d2f0df55140d3f3080284410b1ebfe9df50e3847bcdb1a17bb \
+ --hash=sha256:d355b22270be8062360b9df2ee0b10277590789a1059257916fb9e256a0cff80 \
+ --hash=sha256:d68da281ac208b6236ffa8beef23fe7c7c956d8bf09b421d447232fd94441b35 \
+ --hash=sha256:dafbb58a149d1ebd5767b3f064f911c5ee0897608005e51c3904b0ada2a8d99c \
+ --hash=sha256:dc2b495a92b3971966bf102e73589f950db3ec1ca6713265355ce7771d97b199 \
+ --hash=sha256:dd0f3037d501178a761fd2814370aca547f79e99a200a61a1f6e7b79ec3d78b9 \
+ --hash=sha256:de2c12555bf05546f3ee75733f2c81de70aab7f530e86d0d2162b0ddcb3c39ae \
+ --hash=sha256:e164c4a6868d11988c7f2386e3278ccff21c665cd7d08279b62709dd37a3167c \
+ --hash=sha256:ed59cb4ee258674a6ed9b8515e42079db02cb3be94f133fdd3f200183a40c49d \
+ --hash=sha256:ef07e0b6b879f699a16fc7a513b974246fdeaef44908a021130cf4afe6a1c89c \
+ --hash=sha256:ef2950896673a9809e8e5ab857525924fbcacfc98b858b0509ccf50d341f80f4 \
+ --hash=sha256:f09f145b89ea292cf6c25fd7c8c0d90207fd6a9e54c7828499669996a1e5bdcc \
+ --hash=sha256:f27e7e8a85512b6a4b490d5756edde8e6772883a5800e1476172e44eda3f7d7c \
+ --hash=sha256:f300f28dff74ff4148c21e6971e4de6ca0c34e0086a3d95c017c5174ebf85ca5 \
+ --hash=sha256:f3ed747b7651668131029c720fbc32c7ee2d129bdad200e6c8f4037b20f7913a \
+ --hash=sha256:f87f02afe05c1c246012e3de100a77cfbabd17db709fe93cf074271ab239b312 \
+ --hash=sha256:fa87ea2ad64e59fe5622c1dc5716106b6d6e7e4b26b848cb5f5710a75c292ef5 \
+ --hash=sha256:fc23e72bd7f8a8e540fed2244524d0a9aeb6ae3325f3d56f04a7a3debe5b04c2 \
+ --hash=sha256:fdd00937f15f5f93c8e53d8060a8c4c010431ab595a9b5b50ddd569a04603944 \
+ --hash=sha256:fe7a168d805c8a7bf1c2de55b7ab2bc958586bfd88d0e87235a92cac66b590a7
+yarl==1.25.1 \
+ --hash=sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3 \
+ --hash=sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7 \
+ --hash=sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de \
+ --hash=sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa \
+ --hash=sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62 \
+ --hash=sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec \
+ --hash=sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26 \
+ --hash=sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651 \
+ --hash=sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453 \
+ --hash=sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6 \
+ --hash=sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc \
+ --hash=sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287 \
+ --hash=sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426 \
+ --hash=sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868 \
+ --hash=sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde \
+ --hash=sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9 \
+ --hash=sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7 \
+ --hash=sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0 \
+ --hash=sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf \
+ --hash=sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95 \
+ --hash=sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5 \
+ --hash=sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6 \
+ --hash=sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94 \
+ --hash=sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a \
+ --hash=sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57 \
+ --hash=sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c \
+ --hash=sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee \
+ --hash=sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b \
+ --hash=sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b \
+ --hash=sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c \
+ --hash=sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26 \
+ --hash=sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c \
+ --hash=sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184 \
+ --hash=sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8 \
+ --hash=sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58 \
+ --hash=sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b \
+ --hash=sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d \
+ --hash=sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2 \
+ --hash=sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c \
+ --hash=sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f \
+ --hash=sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3 \
+ --hash=sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa \
+ --hash=sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3 \
+ --hash=sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb \
+ --hash=sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707 \
+ --hash=sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c \
+ --hash=sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345 \
+ --hash=sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40 \
+ --hash=sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492 \
+ --hash=sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371 \
+ --hash=sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec \
+ --hash=sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c \
+ --hash=sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c \
+ --hash=sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25 \
+ --hash=sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac \
+ --hash=sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6 \
+ --hash=sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3 \
+ --hash=sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459 \
+ --hash=sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03 \
+ --hash=sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40 \
+ --hash=sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8 \
+ --hash=sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209 \
+ --hash=sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54 \
+ --hash=sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d \
+ --hash=sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126 \
+ --hash=sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d \
+ --hash=sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9 \
+ --hash=sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0 \
+ --hash=sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225 \
+ --hash=sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414 \
+ --hash=sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059 \
+ --hash=sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d \
+ --hash=sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233 \
+ --hash=sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b \
+ --hash=sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4 \
+ --hash=sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273 \
+ --hash=sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc \
+ --hash=sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865 \
+ --hash=sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc \
+ --hash=sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4 \
+ --hash=sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1 \
+ --hash=sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3 \
+ --hash=sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee \
+ --hash=sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a \
+ --hash=sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df \
+ --hash=sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f \
+ --hash=sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d \
+ --hash=sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449 \
+ --hash=sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e \
+ --hash=sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d \
+ --hash=sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d \
+ --hash=sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35 \
+ --hash=sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420 \
+ --hash=sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b \
+ --hash=sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68 \
+ --hash=sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe \
+ --hash=sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd \
+ --hash=sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b \
+ --hash=sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8 \
+ --hash=sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9 \
+ --hash=sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908 \
+ --hash=sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85 \
+ --hash=sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f \
+ --hash=sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64 \
+ --hash=sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab \
+ --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d \
+ --hash=sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca \
+ --hash=sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1 \
+ --hash=sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7 \
+ --hash=sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3 \
+ --hash=sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa \
+ --hash=sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57 \
+ --hash=sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2 \
+ --hash=sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743 \
+ --hash=sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e \
+ --hash=sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487 \
+ --hash=sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17 \
+ --hash=sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0 \
+ --hash=sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f \
+ --hash=sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6 \
+ --hash=sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3 \
+ --hash=sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad \
+ --hash=sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d \
+ --hash=sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1 \
+ --hash=sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89 \
+ --hash=sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc \
+ --hash=sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f \
+ --hash=sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3 \
+ --hash=sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a \
+ --hash=sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4 \
+ --hash=sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98 \
+ --hash=sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258 \
+ --hash=sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b \
+ --hash=sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f \
+ --hash=sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b \
+ --hash=sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326 \
+ --hash=sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc \
+ --hash=sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75
diff --git a/examples/dsh-agents-as-a-service/gateway/test_gateway.py b/examples/dsh-agents-as-a-service/gateway/test_gateway.py
index 15243af..9bddb65 100644
--- a/examples/dsh-agents-as-a-service/gateway/test_gateway.py
+++ b/examples/dsh-agents-as-a-service/gateway/test_gateway.py
@@ -18,10 +18,12 @@ from types import SimpleNamespace
import httpx
import pytest
from kubernetes.client.exceptions import ApiException
+from starlette.responses import Response
from starlette.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
from gateway import (
+ Session,
Settings,
UserExists,
claim_name,
@@ -219,6 +221,39 @@ def add_user(k8s, user="alice", token=TOKEN, ready=True, pod_name=None, suffix="
return sandbox_name, pod_name
+# --- browser sessions ------------------------------------------------------
+#
+# A browser session is an opaque server-side id. Tests must never forge a
+# cookie (or a bearer token); they mint a real session through the gateway so
+# the production lookup path is exercised.
+
+
+def mint_session(env, user="alice") -> tuple[str, str]:
+ """Mint a server-side session for ``user``; returns ``(cookie_name, sid)``."""
+ response = Response()
+ sid = env.gateway.set_session(response, user)
+ return env.gateway.settings.cookie_name, sid
+
+
+def session_dict(env, user="alice") -> dict:
+ name, sid = mint_session(env, user)
+ return {name: sid}
+
+
+def session_cookie(env, user="alice") -> str:
+ name, sid = mint_session(env, user)
+ return f"{name}={sid}"
+
+
+def response_session_user(env, response) -> str:
+ """The user of the server-side session a response just set."""
+ cookie = response.headers["set-cookie"]
+ value = cookie.split(";", 1)[0].split("=", 1)[1]
+ session = env.gateway.state.sessions.get(value)
+ assert session is not None, "response did not set a server-side session"
+ return session.user
+
+
@pytest.fixture
def env():
settings = Settings(
@@ -463,6 +498,12 @@ def test_settings_from_env(monkeypatch):
monkeypatch.setenv("MAX_BODY_MB", "2")
monkeypatch.setenv("HTTP_TIMEOUT", "12.5")
monkeypatch.setenv("AUTH_PROXY_HEADER", "X-Forwarded-User")
+ monkeypatch.setenv("AUTH_RATE_LIMIT", "5")
+ monkeypatch.setenv("AUTH_RATE_WINDOW", "30")
+ monkeypatch.setenv("SIGNUP_INVITE_CODE", "invite-x")
+ monkeypatch.setenv("UPSTREAM_HEADER_ALLOWLIST", "X-Custom, Accept")
+ monkeypatch.setenv("MAX_IN_FLIGHT_PER_USER", "4")
+ monkeypatch.setenv("WS_MAX_MESSAGE_BYTES", "1024")
settings = Settings.from_env()
assert settings.namespace == "ns-x"
assert settings.pool == "pool-x"
@@ -476,13 +517,56 @@ def test_settings_from_env(monkeypatch):
assert settings.max_body_bytes == 2 * 1024 * 1024
assert settings.http_timeout == 12.5
assert settings.auth_proxy_header == "X-Forwarded-User"
+ assert settings.auth_rate_limit == 5
+ assert settings.auth_rate_window == 30
+ assert settings.signup_invite_code == "invite-x"
+ assert settings.upstream_header_allowlist == frozenset({"x-custom", "accept"})
+ assert settings.max_in_flight_per_user == 4
+ assert settings.ws_max_message_bytes == 1024
def test_settings_auth_proxy_header_defaults_to_none(monkeypatch):
monkeypatch.delenv("AUTH_PROXY_HEADER", raising=False)
+ monkeypatch.setenv("ALLOW_INSECURE_COOKIES", "1")
assert Settings.from_env().auth_proxy_header is None
+def test_settings_from_env_rejects_insecure_cookies_by_default(monkeypatch):
+ monkeypatch.delenv("COOKIE_SECURE", raising=False)
+ monkeypatch.delenv("ALLOW_INSECURE_COOKIES", raising=False)
+ with pytest.raises(RuntimeError, match="insecure session cookie"):
+ Settings.from_env()
+
+
+def test_settings_from_env_allows_insecure_cookies_with_the_explicit_flag(monkeypatch):
+ monkeypatch.delenv("COOKIE_SECURE", raising=False)
+ monkeypatch.setenv("ALLOW_INSECURE_COOKIES", "1")
+ monkeypatch.delenv("COOKIE_NAME", raising=False)
+ settings = Settings.from_env()
+ assert settings.cookie_secure is False
+ assert settings.cookie_name == "dsh_gw_session"
+
+
+def test_settings_default_cookie_name_is_host_prefixed_when_secure(monkeypatch):
+ monkeypatch.setenv("COOKIE_SECURE", "true")
+ monkeypatch.delenv("COOKIE_NAME", raising=False)
+ assert Settings.from_env().cookie_name == "__Host-dsh_gw_session"
+
+
+def test_settings_explicit_cookie_name_wins(monkeypatch):
+ monkeypatch.setenv("COOKIE_SECURE", "true")
+ monkeypatch.setenv("COOKIE_NAME", "sid")
+ assert Settings.from_env().cookie_name == "sid"
+
+
+def test_settings_upstream_header_allowlist_unset_means_denylist_mode(monkeypatch):
+ monkeypatch.setenv("ALLOW_INSECURE_COOKIES", "1")
+ monkeypatch.delenv("UPSTREAM_HEADER_ALLOWLIST", raising=False)
+ assert Settings.from_env().upstream_header_allowlist is None
+ monkeypatch.setenv("UPSTREAM_HEADER_ALLOWLIST", "")
+ assert Settings.from_env().upstream_header_allowlist == frozenset()
+
+
# --- Kubernetes adapter ----------------------------------------------------
@@ -635,11 +719,22 @@ async def test_landing_page_has_signup_and_login_forms(env):
assert "text/html" in response.headers["content-type"]
assert 'action="/users"' in response.text
assert 'action="/login"' in response.text
+ # No invite code configured => no invite input on the landing page.
+ assert 'name="invite"' not in response.text
+
+
+@pytest.mark.asyncio
+async def test_landing_page_shows_invite_input_only_when_configured(env):
+ configure(env, signup_invite_code="let-me-in")
+ async with asgi_client(env) as client:
+ response = await client.get("/")
+ assert response.status_code == 200
+ assert 'name="invite"' in response.text
@pytest.mark.asyncio
async def test_landing_page_shows_session_when_cookie_present(env):
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/")
assert "Signed in as" in response.text
assert "/u/alice/" in response.text
@@ -708,11 +803,14 @@ async def test_signup_json_stores_only_hash_and_returns_token(env, monkeypatch):
assert pool == "test-pool"
assert token_hash == hashlib.sha256(body["token"].encode()).hexdigest()
assert token_hash != body["token"]
- # The JSON API also gets a browser session cookie (HttpOnly, SameSite=Lax).
+ # The JSON API also gets a browser session cookie (HttpOnly, SameSite=Lax)
+ # carrying only an opaque server-side session id, never the user/token.
cookie = response.headers["set-cookie"]
- assert "dsh_gw_session=alice:" in cookie
+ assert "dsh_gw_session=" in cookie
+ assert f"dsh_gw_session={TOKEN}" not in cookie
assert "HttpOnly" in cookie
assert "SameSite=lax" in cookie
+ assert response_session_user(env, response) == "alice"
@pytest.mark.asyncio
@@ -741,13 +839,7 @@ async def test_signup_form_redirects_and_sets_session_cookie(env, monkeypatch):
)
assert response.status_code == 303
assert response.headers["location"] == "/u/bob/"
- cookie = response.headers["set-cookie"]
- assert re_cookie_user(cookie) == "bob"
-
-
-def re_cookie_user(set_cookie: str) -> str:
- value = set_cookie.split(";", 1)[0].split("=", 1)[1]
- return value.split(":", 1)[0]
+ assert response_session_user(env, response) == "bob"
@pytest.mark.asyncio
@@ -774,7 +866,29 @@ async def test_login_form_sets_cookie_and_redirects(env):
)
assert response.status_code == 303
assert response.headers["location"] == "/u/alice/"
- assert re_cookie_user(response.headers["set-cookie"]) == "alice"
+ assert response_session_user(env, response) == "alice"
+
+
+@pytest.mark.asyncio
+async def test_secure_session_cookie_uses_the_host_prefix_and_flags(env):
+ configure(env, cookie_secure=True, cookie_name="__Host-dsh_gw_session")
+ add_user(env.k8s)
+ async with asgi_client(env) as client:
+ response = await client.post(
+ "/login",
+ data={"user": "alice", "token": TOKEN},
+ headers={"content-type": "application/x-www-form-urlencoded"},
+ )
+ cookie = response.headers["set-cookie"]
+ assert cookie.startswith("__Host-dsh_gw_session=")
+ assert "Secure" in cookie
+ assert "HttpOnly" in cookie
+ assert "Path=/" in cookie
+ assert "SameSite=lax" in cookie
+ assert "Domain=" not in cookie
+ # The value is an opaque session id, not :.
+ assert "alice" not in cookie.split(";", 1)[0]
+ assert TOKEN not in cookie
@pytest.mark.asyncio
@@ -786,17 +900,197 @@ async def test_login_json_and_failures(env):
missing = await client.post("/login", json={"user": "ghost", "token": TOKEN})
assert ok.status_code == 200
assert bad.status_code == 401
- assert missing.status_code == 404
+ # An unknown user is a 401 too: login must not reveal which users exist.
+ assert missing.status_code == 401
@pytest.mark.asyncio
-async def test_logout_clears_cookie(env):
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+async def test_logout_clears_cookie_and_revokes_the_session(env):
+ add_user(env.k8s)
+ name, sid = mint_session(env)
+ assert env.gateway.session_for(sid) is not None
+ async with asgi_client(env, cookies={name: sid}) as client:
response = await client.post("/logout")
assert response.status_code == 303
assert response.headers["location"] == "/"
assert "dsh_gw_session=" in response.headers["set-cookie"]
assert "Max-Age=0" in response.headers["set-cookie"]
+ # The old sid is dead server-side, not just deleted from the browser.
+ assert env.gateway.session_for(sid) is None
+ async with asgi_client(env, cookies={name: sid}) as client:
+ replay = await client.get("/users/alice")
+ assert replay.status_code == 401
+
+
+@pytest.mark.asyncio
+async def test_logout_deletion_cookie_is_secure_for_the_host_prefix(env):
+ """A __Host- cookie can only be cleared by a Secure deletion Set-Cookie."""
+ configure(env, cookie_secure=True, cookie_name="__Host-dsh_gw_session")
+ add_user(env.k8s)
+ name, sid = mint_session(env)
+ assert name == "__Host-dsh_gw_session"
+ async with asgi_client(env, cookies={name: sid}) as client:
+ response = await client.post("/logout")
+ assert response.status_code == 303
+ cookie = response.headers["set-cookie"]
+ assert cookie.startswith("__Host-dsh_gw_session=")
+ assert "Max-Age=0" in cookie
+ assert "Path=/" in cookie
+ assert "Secure" in cookie
+ assert "Domain=" not in cookie
+
+
+# --- signup invite / auth abuse controls -----------------------------------
+
+
+def configure(env, **overrides):
+ """Return a copy of the fixture settings with overrides applied."""
+ env.settings = Settings(**{**env.settings.__dict__, **overrides})
+ env.gateway.settings = env.settings
+ return env
+
+
+@pytest.mark.asyncio
+async def test_signup_requires_invite_when_configured(env, monkeypatch):
+ configure(env, signup_invite_code="invite-code")
+
+ async def not_ready(user, timeout):
+ return None
+
+ monkeypatch.setattr(env.gateway, "wait_ready", not_ready)
+ async with asgi_client(env) as client:
+ missing = await client.post("/users", json={"user": "alice"})
+ wrong = await client.post("/users", json={"user": "alice", "invite": "nope"})
+ good = await client.post("/users", json={"user": "alice", "invite": "invite-code"})
+ assert missing.status_code == 403
+ assert wrong.status_code == 403
+ assert good.status_code == 202
+ assert [created[0] for created in env.k8s.created] == ["alice"]
+
+
+@pytest.mark.asyncio
+async def test_invite_is_checked_before_the_user_lookup(env):
+ configure(env, signup_invite_code="invite-code")
+ add_user(env.k8s)
+ async with asgi_client(env) as client:
+ response = await client.post("/users", json={"user": "alice"})
+ # 403, not 409: an unauthenticated caller cannot probe for existing users.
+ assert response.status_code == 403
+
+
+@pytest.mark.asyncio
+async def test_invite_accepts_the_header_and_the_form_field(env, monkeypatch):
+ configure(env, signup_invite_code="invite-code")
+
+ async def not_ready(user, timeout):
+ return None
+
+ monkeypatch.setattr(env.gateway, "wait_ready", not_ready)
+ async with asgi_client(env) as client:
+ via_header = await client.post(
+ "/users",
+ json={"user": "alice"},
+ headers={"X-Signup-Invite": "invite-code"},
+ )
+ via_form = await client.post(
+ "/users",
+ data={"user": "bob", "invite": "invite-code"},
+ headers={"content-type": "application/x-www-form-urlencoded"},
+ )
+ assert via_header.status_code == 202
+ assert via_form.status_code == 303
+
+
+@pytest.mark.asyncio
+async def test_form_auth_rejects_cross_origin_requests(env):
+ add_user(env.k8s)
+ form = {"content-type": "application/x-www-form-urlencoded"}
+ async with asgi_client(env) as client:
+ signup = await client.post(
+ "/users", data={"user": "bob"}, headers={**form, "Origin": "http://evil.test"}
+ )
+ login = await client.post(
+ "/login",
+ data={"user": "alice", "token": TOKEN},
+ headers={**form, "Origin": "http://evil.test"},
+ )
+ referer = await client.post(
+ "/login",
+ data={"user": "alice", "token": TOKEN},
+ headers={**form, "Referer": "http://evil.test/login"},
+ )
+ same_origin = await client.post(
+ "/login",
+ data={"user": "alice", "token": TOKEN},
+ headers={**form, "Origin": "http://gateway.test"},
+ )
+ assert signup.status_code == 403
+ assert login.status_code == 403
+ assert referer.status_code == 403
+ assert same_origin.status_code == 303
+
+
+@pytest.mark.asyncio
+async def test_auth_rate_limit_returns_429_with_retry_after(env):
+ configure(env, auth_rate_limit=2, auth_rate_window=60)
+ add_user(env.k8s)
+ async with asgi_client(env) as client:
+ first = await client.post("/login", json={"user": "alice", "token": "nope"})
+ second = await client.post("/login", json={"user": "alice", "token": "nope"})
+ third = await client.post("/login", json={"user": "alice", "token": "nope"})
+ assert first.status_code == 401
+ assert second.status_code == 401
+ assert third.status_code == 429
+ assert int(third.headers["retry-after"]) >= 1
+
+
+@pytest.mark.asyncio
+async def test_auth_rate_limit_is_per_client_via_rightmost_forwarded_for(env):
+ configure(env, auth_rate_limit=1, auth_rate_window=60)
+ add_user(env.k8s)
+ async with asgi_client(env) as client:
+ first = await client.post(
+ "/login",
+ json={"user": "alice", "token": "nope"},
+ headers={"X-Forwarded-For": "1.1.1.1, 10.0.0.1"},
+ )
+ # A spoofed leftmost entry cannot escape the same rightmost-client bucket.
+ same_client = await client.post(
+ "/login",
+ json={"user": "alice", "token": "nope"},
+ headers={"X-Forwarded-For": "9.9.9.9, 10.0.0.1"},
+ )
+ other_client = await client.post(
+ "/login",
+ json={"user": "alice", "token": "nope"},
+ headers={"X-Forwarded-For": "10.0.0.2"},
+ )
+ assert first.status_code == 401
+ assert same_client.status_code == 429
+ assert other_client.status_code == 401
+
+
+@pytest.mark.asyncio
+async def test_auth_rate_limit_zero_disables_it(env):
+ configure(env, auth_rate_limit=0)
+ add_user(env.k8s)
+ async with asgi_client(env) as client:
+ responses = [
+ await client.post("/login", json={"user": "alice", "token": "nope"}) for _ in range(40)
+ ]
+ assert {response.status_code for response in responses} == {401}
+
+
+@pytest.mark.asyncio
+async def test_signup_is_rate_limited_before_the_body_is_read(env):
+ """429 wins over parsing: an abusive client cannot even submit a body."""
+ configure(env, auth_rate_limit=1, auth_rate_window=60)
+ async with asgi_client(env) as client:
+ # Spend the single allowed auth attempt on a login, then hit signup.
+ await client.post("/login", json={"user": "alice", "token": "nope"})
+ limited = await client.post("/users", content=b"not json at all")
+ assert limited.status_code == 429
+ assert env.k8s.created == []
# --- auth ------------------------------------------------------------------
@@ -810,11 +1104,11 @@ async def test_get_user_requires_valid_bearer_or_cookie(env):
bearer = await client.get("/users/alice", headers={"Authorization": f"Bearer {TOKEN}"})
cookie = await client.get(
"/users/alice",
- headers={"Cookie": f"dsh_gw_session=alice:{TOKEN}"},
+ headers={"Cookie": session_cookie(env)},
)
wrong_user_cookie = await client.get(
"/users/alice",
- headers={"Cookie": f"dsh_gw_session=bob:{TOKEN}"},
+ headers={"Cookie": session_cookie(env, "bob")},
)
assert forbidden.status_code == 401
assert bearer.status_code == 200
@@ -852,7 +1146,7 @@ async def test_delete_user_requires_auth(env):
async def test_delete_with_cookie_requires_same_origin(env):
add_user(env.k8s)
env.gateway.state.handshake[("alice", POD_UID, "gateway.test")] = "dsh-auth-a=x"
- cookies = {"dsh_gw_session": f"alice:{TOKEN}"}
+ cookies = session_dict(env)
async with asgi_client(env, cookies=cookies) as client:
missing = await client.delete("/users/alice")
cross = await client.delete("/users/alice", headers={"Origin": "http://evil.test"})
@@ -890,13 +1184,78 @@ async def test_unexpected_api_error_propagates(env):
await client.get("/users/alice")
+@pytest.mark.asyncio
+async def test_bearer_token_never_authorizes_another_user(env):
+ add_user(env.k8s, "alice", token=TOKEN)
+ add_user(env.k8s, "bob", token="bob-token")
+ async with asgi_client(env) as client:
+ alice_on_bob = await client.get("/users/bob", headers={"Authorization": f"Bearer {TOKEN}"})
+ bob_on_alice = await client.get(
+ "/users/alice", headers={"Authorization": "Bearer bob-token"}
+ )
+ alice_on_alice = await client.get(
+ "/users/alice", headers={"Authorization": f"Bearer {TOKEN}"}
+ )
+ assert alice_on_bob.status_code == 401
+ assert bob_on_alice.status_code == 401
+ assert alice_on_alice.status_code == 200
+
+
+@pytest.mark.asyncio
+async def test_session_cookie_never_authorizes_another_user(env):
+ add_user(env.k8s, "alice", token=TOKEN)
+ add_user(env.k8s, "bob", token="bob-token")
+ bob_cookie = session_cookie(env, "bob")
+ async with asgi_client(env) as client:
+ user = await client.get("/users/alice", headers={"Cookie": bob_cookie})
+ proxy = await client.get("/u/alice/api/state", headers={"Cookie": bob_cookie})
+ assert user.status_code == 401
+ assert proxy.status_code == 401
+
+
+@pytest.mark.asyncio
+async def test_opaque_session_cookie_is_not_a_bearer_token(env):
+ add_user(env.k8s)
+ _name, sid = mint_session(env)
+ async with asgi_client(env) as client:
+ response = await client.get("/users/alice", headers={"Authorization": f"Bearer {sid}"})
+ assert response.status_code == 401
+
+
+@pytest.mark.asyncio
+async def test_expired_session_is_revoked_and_treated_as_anonymous(env):
+ add_user(env.k8s)
+ name, sid = mint_session(env)
+ env.gateway.state.sessions[sid] = Session(user="alice", expires_at=time.time() - 1)
+ async with asgi_client(env, cookies={name: sid}) as client:
+ user = await client.get("/users/alice")
+ landing = await client.get("/")
+ assert user.status_code == 401
+ assert "Signed in as" not in landing.text
+ assert sid not in env.gateway.state.sessions
+
+
+@pytest.mark.asyncio
+async def test_delete_user_revokes_all_its_sessions(env):
+ add_user(env.k8s)
+ cookie = session_dict(env)
+ other = session_dict(env) # a second browser session for the same user
+ async with asgi_client(env, cookies=cookie) as client:
+ response = await client.delete("/users/alice", headers={"Origin": "http://gateway.test"})
+ assert response.status_code == 200
+ assert env.gateway.state.sessions == {}
+ async with asgi_client(env, cookies=other) as client:
+ landing = await client.get("/")
+ assert "Signed in as" not in landing.text
+
+
# --- DSH handshake ---------------------------------------------------------
@pytest.mark.asyncio
async def test_proxy_handshakes_and_uses_browser_host(env):
add_user(env.k8s)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 200
assert response.text == "hello from DSH"
@@ -912,7 +1271,7 @@ async def test_proxy_handshakes_and_uses_browser_host(env):
@pytest.mark.asyncio
async def test_dsh_cookie_is_cached_per_pod_uid_and_host(env):
add_user(env.k8s)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
first = await client.get("/u/alice/api/one")
second = await client.get("/u/alice/api/two")
assert first.status_code == second.status_code == 200
@@ -924,7 +1283,7 @@ async def test_dsh_cookie_is_cached_per_pod_uid_and_host(env):
async def test_missing_launch_token_returns_503_with_retry_after(env):
_sandbox_name, pod_name = add_user(env.k8s)
env.k8s.logs[pod_name] = "no token printed yet\n"
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 503
assert response.headers["retry-after"] == "10"
@@ -935,7 +1294,7 @@ async def test_missing_launch_token_returns_503_with_retry_after(env):
async def test_handshake_4xx_is_503(env):
add_user(env.k8s)
env.upstream.handshake_status = 401
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 503
@@ -944,11 +1303,27 @@ async def test_handshake_4xx_is_503(env):
async def test_missing_cookie_in_handshake_is_503(env):
add_user(env.k8s)
env.upstream.handshake_cookies = ["other=1; Path=/"]
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 503
+@pytest.mark.asyncio
+async def test_handshake_error_never_leaks_the_launch_token_or_url(env):
+ _sandbox_name, pod_name = add_user(env.k8s)
+ secret = "super-secret-launch-token"
+ env.k8s.logs[pod_name] = f"dsh web: http://10.0.0.7:3080/?token={secret}\n"
+ env.upstream.handshake_status = 500
+ async with asgi_client(env, cookies=session_dict(env)) as client:
+ response = await client.get("/u/alice/api/state")
+ assert response.status_code == 503
+ body = response.text
+ assert secret not in body
+ assert FQDN not in body
+ assert "10.0.0.7" not in body
+ assert "dsh-auth" not in body
+
+
@pytest.mark.asyncio
async def test_pod_annotation_overrides_sandbox_name(env):
sandbox_name, pod_name = add_user(env.k8s)
@@ -957,7 +1332,7 @@ async def test_pod_annotation_overrides_sandbox_name(env):
)
env.k8s.pods["custom-pod"] = {"metadata": {"uid": "other-uid"}}
env.k8s.logs["custom-pod"] = env.k8s.logs[pod_name]
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 200
assert ("alice", "other-uid", "gateway.test") in env.gateway.state.handshake
@@ -984,7 +1359,7 @@ async def test_proxy_strips_gateway_auth_cookie_and_hop_headers(env):
env.upstream.proxy_handler = handler
headers = {
"Authorization": f"Bearer {TOKEN}",
- "Cookie": "dsh_gw_session=alice:x; browser=1",
+ "Cookie": session_cookie(env) + "; browser=1",
"X-Forwarded-For": "1.2.3.4",
"X-Forwarded-Proto": "https",
"Host": "gateway.test",
@@ -1009,6 +1384,83 @@ async def test_proxy_strips_gateway_auth_cookie_and_hop_headers(env):
assert "connection" not in response.headers
+@pytest.mark.asyncio
+async def test_proxy_strips_client_supplied_forwarding_and_sso_headers(env):
+ add_user(env.k8s)
+ headers = {
+ "Authorization": f"Bearer {TOKEN}",
+ "Forwarded": "for=evil.test;proto=https",
+ "X-Forwarded-User": "bob",
+ "X-Forwarded-Host": "evil.test",
+ "X-Auth-Request-User": "bob",
+ "X-Auth-Request-Email": "bob@evil.test",
+ "X-Real-IP": "6.6.6.6",
+ "Origin": "http://gateway.test",
+ }
+ async with asgi_client(env) as client:
+ response = await client.get("/u/alice/api/state", headers=headers)
+ assert response.status_code == 200
+ proxy = env.upstream.proxy_requests[0]
+ for name in (
+ "forwarded",
+ "x-forwarded-user",
+ "x-auth-request-user",
+ "x-auth-request-email",
+ "x-real-ip",
+ ):
+ assert name not in proxy.headers
+ # The gateway's own hop metadata comes from the real connection instead.
+ assert proxy.headers["x-forwarded-for"] == "127.0.0.1"
+ assert proxy.headers["x-forwarded-host"] == "gateway.test"
+
+
+@pytest.mark.asyncio
+async def test_proxy_upstream_header_allowlist_drops_unlisted_headers(env):
+ configure(env, upstream_header_allowlist=frozenset({"x-custom-ok"}))
+ add_user(env.k8s)
+ headers = {
+ "Authorization": f"Bearer {TOKEN}",
+ "X-Custom-Ok": "keep",
+ "X-Custom-Drop": "drop",
+ "X-Forwarded-User": "bob",
+ "Origin": "http://gateway.test",
+ }
+ async with asgi_client(env) as client:
+ response = await client.get("/u/alice/api/state", headers=headers)
+ assert response.status_code == 200
+ proxy = env.upstream.proxy_requests[0]
+ assert proxy.headers["x-custom-ok"] == "keep"
+ assert "x-custom-drop" not in proxy.headers
+ # Allowlisting never re-enables gateway-owned or spoofable headers.
+ assert "authorization" not in proxy.headers
+ assert "x-forwarded-user" not in proxy.headers
+ assert "cookie" not in proxy.headers or proxy.headers["cookie"] == "dsh-auth-a=one"
+
+
+@pytest.mark.asyncio
+async def test_proxy_rejects_requests_over_the_per_user_in_flight_cap(env):
+ configure(env, max_in_flight_per_user=1)
+ add_user(env.k8s)
+ env.gateway.state.in_flight["alice"] = 1
+ async with asgi_client(env, cookies=session_dict(env)) as client:
+ response = await client.get("/u/alice/api/state")
+ assert response.status_code == 503
+ assert "retry-after" in response.headers
+ assert env.gateway.state.in_flight["alice"] == 1
+ assert env.upstream.proxy_requests == []
+
+
+@pytest.mark.asyncio
+async def test_proxy_accepts_requests_below_the_per_user_in_flight_cap(env):
+ configure(env, max_in_flight_per_user=2)
+ add_user(env.k8s)
+ env.gateway.state.in_flight["alice"] = 1
+ async with asgi_client(env, cookies=session_dict(env)) as client:
+ response = await client.get("/u/alice/api/state")
+ assert response.status_code == 200
+ assert env.gateway.state.in_flight["alice"] == 1 # the slot was released again
+
+
@pytest.mark.asyncio
async def test_proxy_rewrites_absolute_path_location(env):
add_user(env.k8s)
@@ -1019,7 +1471,7 @@ async def test_proxy_rewrites_absolute_path_location(env):
("set-cookie", "upstream=leak; Path=/"),
],
)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/redirect", follow_redirects=False)
assert response.status_code == 303
assert response.headers["location"] == "/u/alice/?next=1"
@@ -1040,7 +1492,7 @@ async def test_proxy_refreshes_cookie_once_on_upstream_401(env):
return dsh_response(200, b"fresh")
env.upstream.proxy_handler = handler
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 200
assert response.text == "fresh"
@@ -1053,7 +1505,7 @@ async def test_proxy_refreshes_cookie_once_on_upstream_401(env):
async def test_proxy_does_not_refresh_401_without_cached_cookie(env):
add_user(env.k8s)
env.upstream.proxy_handler = lambda request: dsh_response(401, b"no")
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 401
assert len(env.upstream.proxy_requests) == 1
@@ -1067,7 +1519,7 @@ async def test_proxy_upstream_connection_error_is_502(env):
raise httpx.ConnectError("boom", request=request)
env.upstream.proxy_handler = handler
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 502
assert env.gateway.state.in_flight["alice"] == 0
@@ -1106,7 +1558,7 @@ async def test_proxy_client_disconnect_mid_body_returns_499(env):
"/u/alice/api/echo",
headers=[
(b"origin", b"http://gateway.test"),
- (b"cookie", f"dsh_gw_session=alice:{TOKEN}".encode()),
+ (b"cookie", session_cookie(env).encode()),
(b"content-type", b"application/json"),
],
),
@@ -1167,7 +1619,7 @@ async def test_proxy_client_disconnect_mid_stream_frees_slot(env):
raw_scope(
"GET",
"/u/alice/api/stream",
- headers=[(b"cookie", f"dsh_gw_session=alice:{TOKEN}".encode())],
+ headers=[(b"cookie", session_cookie(env).encode())],
),
receive,
)
@@ -1234,7 +1686,7 @@ async def test_proxy_client_disconnect_survives_cancelled_upstream_close(env):
raw_scope(
"GET",
"/u/alice/api/stream",
- headers=[(b"cookie", f"dsh_gw_session=alice:{TOKEN}".encode())],
+ headers=[(b"cookie", session_cookie(env).encode())],
),
receive,
)
@@ -1258,7 +1710,7 @@ async def test_proxy_wakes_suspended_sandbox(env, monkeypatch):
return env.k8s.sandboxes[sandbox_name]
monkeypatch.setattr(env.gateway, "wait_ready", ready)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 200
assert ("dsh-alice-sbx", "Running") in env.k8s.patches
@@ -1273,7 +1725,7 @@ async def test_proxy_wake_timeout_returns_503(env, monkeypatch):
return None
monkeypatch.setattr(env.gateway, "wait_ready", never)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/u/alice/api/state")
assert response.status_code == 503
assert response.headers["retry-after"] == "10"
@@ -1297,7 +1749,7 @@ async def test_proxy_requires_auth(env):
@pytest.mark.asyncio
async def test_proxy_cookie_mutation_requires_same_origin(env):
add_user(env.k8s)
- cookies = {"dsh_gw_session": f"alice:{TOKEN}"}
+ cookies = session_dict(env)
async with asgi_client(env, cookies=cookies) as client:
cross = await client.post(
"/u/alice/api/mutate", headers={"Origin": "http://evil.test"}, content=b"{}"
@@ -1385,7 +1837,7 @@ def test_ws_cookie_cross_origin_closes_4401(env):
with client.websocket_connect(
"/u/alice/api/remote.mux",
headers={
- "cookie": f"dsh_gw_session=alice:{TOKEN}",
+ "cookie": session_cookie(env),
"origin": "http://evil.test",
},
) as ws:
@@ -1425,7 +1877,7 @@ def test_ws_cookie_same_origin_bridges_frames(env):
with client.websocket_connect(
"/u/alice/api/remote.mux",
headers={
- "cookie": f"dsh_gw_session=alice:{TOKEN}",
+ "cookie": session_cookie(env),
"origin": "http://testserver",
},
) as ws:
@@ -1464,6 +1916,70 @@ def test_ws_upstream_connect_failure_closes_1011(env):
assert exc.value.code == 1011
+def test_ws_strips_client_supplied_forwarding_and_sso_headers(env):
+ add_user(env.k8s)
+ with TestClient(env.app) as client:
+ with client.websocket_connect(
+ "/u/alice/api/remote.mux",
+ headers={
+ "Authorization": f"Bearer {TOKEN}",
+ "Forwarded": "for=evil.test",
+ "X-Forwarded-User": "bob",
+ "X-Auth-Request-User": "bob",
+ },
+ ) as ws:
+ ws.send_text("hello")
+ assert ws.receive_text() == "welcome"
+ assert ws.receive_text() == "pong"
+ headers = env.ws_connect.calls[0][1]["additional_headers"]
+ assert "forwarded" not in headers
+ assert "x-forwarded-user" not in headers
+ assert "x-auth-request-user" not in headers
+ assert headers["cookie"] == "dsh-auth-a=one"
+
+
+def test_ws_per_user_in_flight_cap_closes_1011(env):
+ configure(env, max_in_flight_per_user=1)
+ add_user(env.k8s)
+ env.gateway.state.in_flight["alice"] = 1
+ with TestClient(env.app) as client:
+ with client.websocket_connect(
+ "/u/alice/api/remote.mux",
+ headers={"Authorization": f"Bearer {TOKEN}"},
+ ) as ws:
+ with pytest.raises(WebSocketDisconnect) as exc:
+ ws.receive_text()
+ assert exc.value.code == 1011
+ assert env.ws_connect.calls == []
+
+
+def test_ws_passes_the_default_max_message_size(env):
+ add_user(env.k8s)
+ with TestClient(env.app) as client:
+ with client.websocket_connect(
+ "/u/alice/api/remote.mux",
+ headers={"Authorization": f"Bearer {TOKEN}"},
+ ) as ws:
+ ws.send_text("hello")
+ assert ws.receive_text() == "welcome"
+ assert ws.receive_text() == "pong"
+ assert env.ws_connect.calls[0][1]["max_size"] == 16 * 1024 * 1024
+
+
+def test_ws_max_message_size_is_configurable(env):
+ configure(env, ws_max_message_bytes=2048)
+ add_user(env.k8s)
+ with TestClient(env.app) as client:
+ with client.websocket_connect(
+ "/u/alice/api/remote.mux",
+ headers={"Authorization": f"Bearer {TOKEN}"},
+ ) as ws:
+ ws.send_text("hello")
+ assert ws.receive_text() == "welcome"
+ assert ws.receive_text() == "pong"
+ assert env.ws_connect.calls[0][1]["max_size"] == 2048
+
+
# --- idle sweeper ----------------------------------------------------------
@@ -1576,14 +2092,46 @@ async def test_sweeper_survives_transient_api_error(env):
assert env.gateway.state.maintenance["alice"] is False
+@pytest.mark.asyncio
+async def test_sweeper_prunes_orphan_and_expired_state(env):
+ now = time.time()
+ # No claim for "ghost": every per-user entry is orphaned.
+ env.gateway.state.last_activity["ghost"] = now
+ env.gateway.state.in_flight["ghost"] = 0
+ env.gateway.state.maintenance["ghost"] = False
+ env.gateway.state.handshake[("ghost", POD_UID, "gateway.test")] = "dsh-auth-a=x"
+ _ghost_name, ghost_sid = mint_session(env, "ghost")
+ env.gateway.state.auth_rate["1.2.3.4"] = (now - 3600, 5)
+ # A user with requests still in flight is preserved even without a claim.
+ env.gateway.state.in_flight["busy"] = 2
+ # A live claim keeps its user's current session...
+ env.k8s.claims["alice"] = ready_claim()
+ env.k8s.sandboxes["dsh-alice-sbx"] = ready_sandbox()
+ env.gateway.state.last_activity["alice"] = now
+ _alice_name, alice_sid = mint_session(env, "alice")
+ # ...but an expired session is pruned even for a live user.
+ _old_name, expired_sid = mint_session(env, "alice")
+ env.gateway.state.sessions[expired_sid] = Session(user="alice", expires_at=now - 1)
+
+ await env.gateway.sweep_once()
+
+ assert "ghost" not in env.gateway.state.last_activity
+ assert "ghost" not in env.gateway.state.in_flight
+ assert "ghost" not in env.gateway.state.maintenance
+ assert ("ghost", POD_UID, "gateway.test") not in env.gateway.state.handshake
+ assert ghost_sid not in env.gateway.state.sessions
+ assert env.gateway.state.auth_rate == {}
+ assert env.gateway.state.in_flight["busy"] == 2
+ assert alice_sid in env.gateway.state.sessions
+ assert expired_sid not in env.gateway.state.sessions
+
+
# --- SSO (reverse-proxy identity) ------------------------------------------
def enable_sso(env, header="X-Forwarded-User"):
"""Flip an env into SSO mode: the gateway trusts the named identity header."""
- env.settings = Settings(**{**env.settings.__dict__, "auth_proxy_header": header})
- env.gateway.settings = env.settings
- return env
+ return configure(env, auth_proxy_header=header)
def test_remote_user_none_when_sso_disabled(env):
@@ -1611,7 +2159,7 @@ async def test_sso_landing_new_user_shows_provisioning(env):
assert "Signed in as alice" in response.text
assert "Provisioning" in response.text
assert "Recreate sandbox" not in response.text
- assert re_cookie_user(response.headers["set-cookie"]) == "alice"
+ assert response_session_user(env, response) == "alice"
assert [c[0] for c in env.k8s.created] == ["alice"]
assert env.k8s.token_patches == []
@@ -1620,7 +2168,7 @@ async def test_sso_landing_new_user_shows_provisioning(env):
async def test_sso_landing_ready_connects_and_offers_recreate_only(env):
enable_sso(env)
add_user(env.k8s)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.get("/", headers={"X-Forwarded-User": "alice"})
assert response.status_code == 200
assert "Signed in as alice" in response.text
@@ -1642,11 +2190,55 @@ async def test_sso_landing_rotates_token_when_session_missing(env):
response = await client.get("/", headers={"X-Forwarded-User": "alice"})
assert response.status_code == 200
assert "Recreate sandbox" in response.text
- assert re_cookie_user(response.headers["set-cookie"]) == "alice"
+ assert response_session_user(env, response) == "alice"
assert [p[0] for p in env.k8s.token_patches] == ["alice"]
assert env.k8s.created == []
+@pytest.mark.asyncio
+async def test_sso_landing_reuses_a_valid_session_for_the_remote_user(env):
+ enable_sso(env)
+ add_user(env.k8s)
+ name, sid = mint_session(env)
+ async with asgi_client(env, cookies={name: sid}) as client:
+ response = await client.get("/", headers={"X-Forwarded-User": "alice"})
+ assert response.status_code == 200
+ assert "Recreate sandbox" in response.text
+ # Untouched: no cookie is re-issued and no token is rotated.
+ assert "set-cookie" not in response.headers
+ assert [p[0] for p in env.k8s.token_patches] == []
+ assert env.gateway.state.sessions[sid].user == "alice"
+
+
+@pytest.mark.asyncio
+async def test_sso_landing_revokes_a_foreign_session(env):
+ enable_sso(env)
+ add_user(env.k8s)
+ bob_name, bob_sid = mint_session(env, "bob")
+ async with asgi_client(env, cookies={bob_name: bob_sid}) as client:
+ response = await client.get("/", headers={"X-Forwarded-User": "alice"})
+ assert response.status_code == 200
+ # A session for another identity is never reused and is revoked server-side.
+ assert bob_sid not in env.gateway.state.sessions
+ assert response_session_user(env, response) == "alice"
+
+
+@pytest.mark.asyncio
+async def test_recreate_revokes_the_old_session_and_mints_a_new_one(env):
+ enable_sso(env)
+ add_user(env.k8s)
+ name, sid = mint_session(env)
+ async with asgi_client(env, cookies={name: sid}) as client:
+ response = await client.post(
+ "/recreate",
+ headers={"X-Forwarded-User": "alice", "Origin": "http://gateway.test"},
+ )
+ assert response.status_code == 303
+ assert sid not in env.gateway.state.sessions
+ new_sid = response.headers["set-cookie"].split(";", 1)[0].split("=", 1)[1]
+ assert env.gateway.state.sessions[new_sid].user == "alice"
+
+
@pytest.mark.asyncio
async def test_recreate_deletes_and_reprovisions(env):
enable_sso(env)
@@ -1657,7 +2249,7 @@ async def test_recreate_deletes_and_reprovisions(env):
assert response.headers["location"] == "/u/alice/"
assert env.k8s.deleted == ["alice"]
assert [c[0] for c in env.k8s.created] == ["alice"]
- assert re_cookie_user(response.headers["set-cookie"]) == "alice"
+ assert response_session_user(env, response) == "alice"
@pytest.mark.asyncio
@@ -1673,7 +2265,7 @@ async def test_recreate_requires_sso(env):
async def test_recreate_rejects_cross_origin(env):
enable_sso(env)
add_user(env.k8s)
- async with asgi_client(env, cookies={"dsh_gw_session": f"alice:{TOKEN}"}) as client:
+ async with asgi_client(env, cookies=session_dict(env)) as client:
response = await client.post(
"/recreate",
headers={"X-Forwarded-User": "alice", "Origin": "http://evil.test"},
diff --git a/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml b/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
index 4ab56f0..432298b 100644
--- a/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
@@ -131,6 +131,24 @@ spec:
# a private PVC), so trade DSH's inner file policy for a working
# agent. Set this to workspace-write (or read-only) on hosts where a
# backend is available; see the README troubleshooting row.
+ # SECURITY: the pod is the isolation boundary in this example.
+ # danger-full-access is set only because DSH's own command sandbox
+ # has no usable backend in this pod (see above), which means an
+ # agent command can read its own DSH_HOME (including
+ # .credentials.yaml) and write anywhere in the container. What
+ # still contains it is the pod itself: one pod per user, non-root,
+ # all capabilities dropped, RuntimeDefault seccomp, its own PVC and
+ # Service. That is NOT a hardened multi-tenant boundary on a shared
+ # kernel. For production/multi-tenant use, additionally run the
+ # sandbox under gVisor (`runtimeClassName: gvisor` above) so a
+ # container escape is trapped in a userspace kernel, and apply the
+ # sandbox NetworkPolicy (manifests/optional/network-policy/) so
+ # tenants cannot reach each other or cluster-internal endpoints.
+ # The base manifests without those two controls are a demo. Treat
+ # danger-full-access as a fallback, not a target: on hosts where
+ # DSH's command-sandbox backend works, prefer workspace-write (or
+ # read-only), and recreate the pod because the value is stamped at
+ # creation.
- name: DSH_PERMISSION_MODE
value: danger-full-access
# Probes target /favicon.svg: DSH gates / behind its launch-token
diff --git a/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml b/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
index 39b2ff1..f5e51cb 100644
--- a/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
@@ -134,14 +134,30 @@ spec:
# the DSH launch token.
- name: DSH_CONTAINER
value: dsh-web
- # Cookie dsh_gw_session is HttpOnly + SameSite=Lax. "false" keeps
- # the plain-HTTP demo usable through port-forward: a Secure cookie
- # is never sent over HTTP, so login would silently fail.
- # HTTPS deployments (the ingress/ variant with TLS, or
- # gateway-api-sso/, which terminates TLS at the Gateway) MUST set
- # this to "true".
+ # Browser session cookie: HttpOnly + SameSite=Lax, holds only an
+ # opaque, server-side session id (sid) - never the user's DSH
+ # token. Sessions can be revoked server-side: POST /logout, user
+ # delete, POST /recreate and expiry (COOKIE_MAX_AGE, default
+ # 7 days) all invalidate them. The session table is in process
+ # memory, so restarting this Deployment logs every browser out.
+ # With COOKIE_SECURE=true the cookie is named
+ # __Host-dsh_gw_session; COOKIE_NAME overrides the name.
+ #
+ # "false" keeps the plain-HTTP demo usable through port-forward: a
+ # Secure cookie is never sent over HTTP, so login would silently
+ # fail. The gateway REFUSES TO START with COOKIE_SECURE=false
+ # unless ALLOW_INSECURE_COOKIES=1 acknowledges that the session
+ # travels in clear text, so the base demo sets both. HTTPS
+ # deployments (the ingress/ variant with TLS, or gateway-api-sso/,
+ # which terminates TLS at the Gateway) MUST set this to "true".
- name: COOKIE_SECURE
value: "false"
+ # Explicit acknowledgement that this base demo serves plain HTTP.
+ # Required when COOKIE_SECURE=false (the gateway exits at startup
+ # otherwise); ignored once COOKIE_SECURE=true, so HTTPS installs
+ # should drop it. Never set this on an internet-facing deployment.
+ - name: ALLOW_INSECURE_COOKIES
+ value: "1"
# SSO IS OPT-IN AND DISABLED HERE. With AUTH_PROXY_HEADER unset the
# gateway trusts only its own bearer/session auth and ignores every
# identity header. Enable it ONLY behind
@@ -152,9 +168,49 @@ spec:
# reach :8080 can impersonate any user by sending that header.
# - name: AUTH_PROXY_HEADER
# value: X-Forwarded-User
+ # Brute-force / abuse throttle for the unauthenticated auth
+ # endpoints (/users and /login): at most N attempts per client per
+ # AUTH_RATE_WINDOW seconds (default 60). A client is the rightmost
+ # X-Forwarded-For entry (the one added by the trusted proxy) when
+ # present, otherwise the TCP peer. Over the limit the gateway
+ # answers 429 + Retry-After BEFORE reading the body or touching the
+ # cluster; 0 disables the limiter. This is a per-process bucket, so
+ # it is not a substitute for an ingress rate limit.
+ - name: AUTH_RATE_LIMIT
+ value: "30"
+ # Window in seconds for AUTH_RATE_LIMIT (default 60).
+ # - name: AUTH_RATE_WINDOW
+ # value: "60"
+ # Optional signup gate. When set, POST /users requires the invite
+ # code - form field "invite" or header X-Signup-Invite - compared in
+ # constant time. A missing/wrong code is rejected with 403 before
+ # the user is looked up, so it also stops unauthenticated claim
+ # creation. The landing page shows the invite input only when this
+ # is set. Keep the value in a Secret, create it before rollout and
+ # restart the gateway to pick up a rotation:
+ # kubectl -n dsh-demo create secret generic dsh-signup-invite \
+ # --from-literal=code="$(openssl rand -hex 16)"
+ # - name: SIGNUP_INVITE_CODE
+ # valueFrom:
+ # secretKeyRef:
+ # name: dsh-signup-invite
+ # key: code
# Reject proxied request bodies larger than this (413).
- name: MAX_BODY_MB
value: "32"
+ # Per-user concurrency guard (default 32): at most this many
+ # proxied HTTP requests and WebSocket connections per user.
+ # Excess HTTP requests get 503 + Retry-After and excess WebSocket
+ # upgrades close with 1011, so one tenant cannot exhaust the
+ # single-replica gateway for everyone else.
+ # - name: MAX_IN_FLIGHT_PER_USER
+ # value: "32"
+ # Largest WebSocket message received from DSH, in bytes (default
+ # 16 MiB); oversized frames close the connection. Browser-to-DSH
+ # frames are bounded by uvicorn's own limit. HTTP/SSE responses are
+ # streamed without this cap.
+ # - name: WS_MAX_MESSAGE_BYTES
+ # value: "16777216"
# /healthz is unauthenticated and returns 200 without touching the
# cluster, so it is safe for both readiness and liveness.
readinessProbe:
diff --git a/examples/dsh-agents-as-a-service/manifests/exposure/README.md b/examples/dsh-agents-as-a-service/manifests/exposure/README.md
index a796575..e3f1537 100644
--- a/examples/dsh-agents-as-a-service/manifests/exposure/README.md
+++ b/examples/dsh-agents-as-a-service/manifests/exposure/README.md
@@ -16,6 +16,16 @@ Applying both gives the gateway two entry points, and the SSO variant is only
safe when the gateway is reachable **exclusively** through oauth2-proxy. Do not
mix them.
+> **Production requirement.** Each variant's NetworkPolicy is part of the
+> variant, and applying the variant without it is demo-only. The sandbox
+> NetworkPolicy and the gateway egress policy (the API server is cluster-specific)
+> are separate and equally required; see the
+> [production checklist](../../docs/optional-hardening.md#6-production-checklist)
+> and the
+> [gateway egress recipe](../../docs/optional-hardening.md#gateway-egress-required-for-production).
+> For SSO, run [`scripts/preflight-sso.sh`](../../scripts/preflight-sso.sh)
+> after applying the variant and before sending real traffic.
+
Each variant also ships a NetworkPolicy, applied by the same directory apply:
- `ingress/61-networkpolicy.yaml` allows only Traefik pods to reach the gateway
@@ -27,7 +37,8 @@ Each variant also ships a NetworkPolicy, applied by the same directory apply:
The policies assume Traefik's `app.kubernetes.io/name: traefik` pod label.
Adjust the selector for another implementation or a `hostNetwork` install; see
[optional hardening](../../docs/optional-hardening.md) for the probe caveat and
-verification commands.
+verification commands. Keep the policies: an exposure without its NetworkPolicy
+is demo-only.
## Applying a variant
@@ -69,10 +80,13 @@ Secrets imperatively (commands below) before applying the directory.
2. Set `DSH_TRUSTED_HOSTS` in the SandboxTemplate to the same public host:
the gateway forwards the browser's `Host`, and DSH rejects unknown hosts on
`/api` (see `../10-sandboxtemplate.yaml`).
-3. Set `COOKIE_SECURE=true` on the gateway once the Ingress serves HTTPS:
- `kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true`. The
- base manifest keeps `false` so the plain-HTTP port-forward demo works; a
- `Secure` cookie is never sent over HTTP, which silently breaks login.
+3. Set `COOKIE_SECURE=true` and remove the plain-HTTP acknowledgement once the
+ Ingress serves HTTPS:
+ `kubectl -n dsh-demo set env deploy/dsh-gateway COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-`.
+ The base manifest keeps `false` + `ALLOW_INSECURE_COOKIES=1` so the
+ plain-HTTP port-forward demo works; a `Secure` cookie is never sent over HTTP,
+ which silently breaks login, while the acknowledgement must not survive into
+ production.
4. Do not attach buffering or compression middlewares to this router: they
break WebSocket upgrades and streaming.
5. `61-networkpolicy.yaml` is part of this variant: it allows only Traefik pods
@@ -116,14 +130,23 @@ kubectl -n dsh-demo create secret tls dsh-demo-tls --cert=tls.crt --key=tls.key
# 3. Enable SSO on the gateway and trust the public host in the template.
kubectl -n dsh-demo set env deploy/dsh-gateway \
- AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true
+ AUTH_PROXY_HEADER=X-Forwarded-User COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-
# then set DSH_TRUSTED_HOSTS=dsh-demo.example.com in the SandboxTemplate
# (see ../10-sandboxtemplate.yaml) and re-apply it.
# 4. Apply the variant.
kubectl apply -f manifests/exposure/gateway-api-sso/
+
+# 5. Preflight: fails loudly on missing Secrets, a bad oauth2-proxy cookie
+# secret, missing gateway SSO env, a dangling TLS reference, leftover
+# placeholders, or a second path to the gateway.
+./scripts/preflight-sso.sh
```
+The preflight is read-only and exits non-zero on any failure, naming the exact
+fix. Run it again after rotating a Secret or changing the public host; it also
+verifies that the oauth2-proxy cookie secret decodes to 16, 24 or 32 bytes.
+
### SSO trust boundary
`AUTH_PROXY_HEADER` is opt-in and deliberately unset in the base gateway
@@ -141,8 +164,14 @@ takes all of these:
impersonate any user.
3. Keep `75-networkpolicies.yaml` with the variant. It enforces the boundary in
the network layer (gateway ← oauth2-proxy ← Traefik, and limited oauth2-proxy
- egress). A custom exposure without an equivalent policy has no network
+ egress). This is required, not optional: an exposure without the policy is
+ demo-only, and a custom exposure without an equivalent policy has no network
enforcement.
+4. Apply the gateway egress policy too: the gateway talks to the Kubernetes API
+ and to sandbox pods, and `75-networkpolicies.yaml` intentionally leaves that
+ direction unrestricted because the API-server endpoint is cluster-specific.
+ See
+ [gateway egress](../../docs/optional-hardening.md#gateway-egress-required-for-production).
Also terminate TLS in front of the gateway (the Gateway listener does) and
leave `COOKIE_SECURE=true` so the session cookie never travels in clear text.
diff --git a/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/72-oauth2-proxy.yaml b/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/72-oauth2-proxy.yaml
index 881f852..4c6f9a0 100644
--- a/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/72-oauth2-proxy.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/72-oauth2-proxy.yaml
@@ -25,8 +25,9 @@
# TLS terminates at the Gateway listener (70-gateway.yaml); keep
# --cookie-secure=true and COOKIE_SECURE=true on the gateway.
#
-# The image is pinned to quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
-# (versions.env: OAUTH2_PROXY_IMAGE). Credentials come from the
+# The image is digest-pinned: quay.io/oauth2-proxy/oauth2-proxy:v7.15.5@sha256:...
+# (versions.env: OAUTH2_PROXY_IMAGE). The digest is the immutable multi-arch
+# index, so the tag can never be moved under us. Credentials come from the
# "oauth2-proxy" Secret (see 73-oauth2-proxy-secret.yaml.example); this
# manifest never carries secrets.
apiVersion: apps/v1
@@ -61,7 +62,7 @@ spec:
type: RuntimeDefault
containers:
- name: oauth2-proxy
- image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.5@sha256:8498b0d0ef0a7b29686414000a08aee467f02d0299c9ed1e006a8f33fc017916
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
diff --git a/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/75-networkpolicies.yaml b/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/75-networkpolicies.yaml
index 289ceee..42ac27b 100644
--- a/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/75-networkpolicies.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/75-networkpolicies.yaml
@@ -1,5 +1,5 @@
-# OPTIONAL with the SSO exposure variant: enforce the trust boundary at the
-# network layer.
+# REQUIRED with the SSO exposure variant: enforce the trust boundary at the
+# network layer. Applying the SSO variant without this file is demo-only.
#
# The chain is Traefik (Gateway API) -> oauth2-proxy -> dsh-gateway. These
# policies make it the ONLY chain:
@@ -8,8 +8,9 @@
# - oauth2-proxy egress is limited to DNS, public HTTPS (GitHub) and the
# gateway.
# The gateway's own egress (Kubernetes API, sandbox pods) is intentionally not
-# restricted here: the API-server endpoint is cluster-specific. See
-# ../../../docs/optional-hardening.md for that recipe.
+# restricted here: the API-server endpoint is cluster-specific. Add it for
+# production. See
+# ../../../docs/optional-hardening.md#gateway-egress-required-for-production.
#
# This manifest assumes Traefik, matching gatewayClassName: traefik in
# 70-gateway.yaml. Adjust the app.kubernetes.io/name selector for another
diff --git a/examples/dsh-agents-as-a-service/manifests/exposure/ingress/60-ingress.yaml b/examples/dsh-agents-as-a-service/manifests/exposure/ingress/60-ingress.yaml
index 55760be..60657ec 100644
--- a/examples/dsh-agents-as-a-service/manifests/exposure/ingress/60-ingress.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/exposure/ingress/60-ingress.yaml
@@ -14,8 +14,13 @@
# DSH_TRUSTED_HOSTS to this host so DSH accepts the forwarded Host/Origin
# on /api.
# 2. Gateway Deployment (manifests/50-gateway.yaml): set COOKIE_SECURE to
-# "true" once this Ingress serves HTTPS, so the session cookie is Secure.
-# The base manifest sets "false" for the plain-HTTP port-forward demo.
+# "true" and remove ALLOW_INSECURE_COOKIES once this Ingress serves HTTPS,
+# so the session cookie is Secure:
+# kubectl -n dsh-demo set env deploy/dsh-gateway \
+# COOKIE_SECURE=true ALLOW_INSECURE_COOKIES-
+# The base manifest sets "false" + ALLOW_INSECURE_COOKIES=1 for the
+# plain-HTTP port-forward demo; without the acknowledgement the gateway
+# refuses to start.
#
# WebSocket support is REQUIRED: the browser talks to /u//... and DSH
# uses WebSockets for live session traffic. Traefik upgrades WS/SSE
@@ -24,9 +29,11 @@
# and upgrades. `path: /` Prefix covers the landing page and every per-user
# proxy path, so no per-sandbox Ingress is needed.
#
-# 61-networkpolicy.yaml is part of this variant: it restricts the gateway's
-# ingress to Traefik pods. Adjust its selector if your ingress controller uses
-# a different label or hostNetwork.
+# 61-networkpolicy.yaml is part of this variant and required for production:
+# it restricts the gateway's ingress to Traefik pods. Adjust its selector if
+# your ingress controller uses a different label or hostNetwork. The sandbox
+# and gateway-egress NetworkPolicies are separate requirements; see
+# ../../../docs/optional-hardening.md#6-production-checklist.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
diff --git a/examples/dsh-agents-as-a-service/manifests/exposure/ingress/61-networkpolicy.yaml b/examples/dsh-agents-as-a-service/manifests/exposure/ingress/61-networkpolicy.yaml
index 15a4be2..c072cf6 100644
--- a/examples/dsh-agents-as-a-service/manifests/exposure/ingress/61-networkpolicy.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/exposure/ingress/61-networkpolicy.yaml
@@ -1,11 +1,16 @@
-# OPTIONAL with the Ingress exposure variant: restrict the gateway's ingress
-# to the ingress controller.
+# REQUIRED with the Ingress exposure variant for production: restrict the
+# gateway's ingress to the ingress controller. Applying the variant without
+# this policy is demo-only.
#
# This manifest assumes Traefik, matching this variant's
# `ingressClassName: traefik`. Traefik's Helm chart labels its pods
# app.kubernetes.io/name=traefik; adjust the selector for another controller or
# for a hostNetwork install (where the source is the node IP, not a pod IP).
#
+# The sandbox NetworkPolicy and the gateway egress policy are separate and also
+# required for production; see
+# ../../../docs/optional-hardening.md#6-production-checklist.
+#
# NetworkPolicy enforcement of kubelet probe traffic is CNI-dependent. If the
# gateway starts failing its /healthz probes, allow the node CIDR on port 8080
# or remove this policy. See ../../../docs/optional-hardening.md.
diff --git a/examples/dsh-agents-as-a-service/manifests/optional/README.md b/examples/dsh-agents-as-a-service/manifests/optional/README.md
index 41fb21f..fe9443e 100644
--- a/examples/dsh-agents-as-a-service/manifests/optional/README.md
+++ b/examples/dsh-agents-as-a-service/manifests/optional/README.md
@@ -23,6 +23,12 @@ It requires a CNI that enforces `NetworkPolicy`. The selector uses the
agent-sandbox controller's pod tracking label, so the policy applies to
existing warm-pool and claimed sandboxes without recreation.
+**This is opt-in for the demo but required for production.** Without it every
+tenant can reach every other tenant and cluster-internal endpoints. Production
+also needs the gateway egress policy (cluster-specific API-server endpoint) and
+the exposure variant's policy; see the
+[production checklist](../../docs/optional-hardening.md#6-production-checklist).
+
## gVisor, nodeSelector and provider keys
These are configured in the main manifests or with an extra Secret:
diff --git a/examples/dsh-agents-as-a-service/manifests/optional/network-policy/10-sandbox-networkpolicy.yaml b/examples/dsh-agents-as-a-service/manifests/optional/network-policy/10-sandbox-networkpolicy.yaml
index ee087bd..1aa7034 100644
--- a/examples/dsh-agents-as-a-service/manifests/optional/network-policy/10-sandbox-networkpolicy.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/optional/network-policy/10-sandbox-networkpolicy.yaml
@@ -1,9 +1,15 @@
-# OPTIONAL, opt-in tenant network policy for the DSH sandboxes.
+# OPTIONAL for the base walkthrough, REQUIRED for production. Opt-in tenant
+# network policy for the DSH sandboxes: without it every tenant can reach every
+# other tenant and cluster-internal endpoints.
#
# It is NOT part of the top-level walkthrough. Apply and delete it explicitly:
# kubectl apply -f manifests/optional/network-policy/
# kubectl delete -f manifests/optional/network-policy/
#
+# Production also needs the gateway egress policy (cluster-specific API-server
+# endpoint) and the exposure variant's policy, plus gVisor
+# (docs/optional-hardening.md#6-production-checklist).
+#
# The selector uses the agent-sandbox controller's pod tracking label, which is
# present on every Sandbox pod (including warm-pool spares), so the policy
# applies to existing sandboxes without recreating them. The SandboxTemplate
diff --git a/examples/dsh-agents-as-a-service/scripts/preflight-sso.sh b/examples/dsh-agents-as-a-service/scripts/preflight-sso.sh
new file mode 100755
index 0000000..084ff9e
--- /dev/null
+++ b/examples/dsh-agents-as-a-service/scripts/preflight-sso.sh
@@ -0,0 +1,234 @@
+#!/usr/bin/env bash
+# Preflight for the gateway-api-sso exposure variant.
+#
+# Run this AFTER creating the oauth2-proxy/TLS Secrets, setting the gateway
+# env and applying manifests/exposure/gateway-api-sso/, and BEFORE sending real
+# traffic. It checks the configuration that is easy to get wrong and whose
+# failure is either silent (login appears to work but the gateway still asks
+# for its own token) or dangerous (a spoofable identity header, a placeholder
+# host, a TLS Secret that does not exist).
+#
+# Checks:
+# 1. kubectl reaches the cluster and the SSO objects exist;
+# 2. the oauth2-proxy Secret has client-id, client-secret and a cookie-secret
+# that decodes to 16, 24 or 32 bytes; the TLS Secret exists;
+# 3. deploy/dsh-gateway has AUTH_PROXY_HEADER=X-Forwarded-User and
+# COOKIE_SECURE=true (both are opt-in and both are required for SSO over
+# TLS);
+# 4. the Gateway's TLS certificate reference resolves to an existing Secret;
+# 5. no your-github-org / dsh-demo.example.com placeholder is left in the
+# applied oauth2-proxy args, Gateway listeners or HTTPRoute hostnames;
+# 6. (warning) no Ingress in the namespace creates a second path to
+# dsh-gateway. With SSO enabled the Service must be reachable only through
+# oauth2-proxy.
+#
+# Exit status: 0 when every check passes (warnings allowed), 1 on any failure.
+# Every failure prints the fix. This script only reads cluster state.
+#
+# Usage:
+# examples/dsh-agents-as-a-service/scripts/preflight-sso.sh
+#
+# Environment overrides:
+# NAMESPACE namespace (default: dsh-demo)
+# GATEWAY_DEPLOY gateway Deployment name (default: dsh-gateway)
+# OAUTH_DEPLOY oauth2-proxy Deployment name (default: oauth2-proxy)
+# OAUTH_SECRET oauth2-proxy Secret name (default: oauth2-proxy)
+# TLS_SECRET TLS Secret name the Gateway should reference (default:
+# dsh-demo-tls)
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+NS="${NAMESPACE:-dsh-demo}"
+GATEWAY_DEPLOY="${GATEWAY_DEPLOY:-dsh-gateway}"
+OAUTH_DEPLOY="${OAUTH_DEPLOY:-oauth2-proxy}"
+OAUTH_SECRET="${OAUTH_SECRET:-oauth2-proxy}"
+TLS_SECRET="${TLS_SECRET:-dsh-demo-tls}"
+
+FAILED=0
+pass() { printf 'PASS %s\n' "$*"; }
+warn() { printf 'WARN %s\n' "$*" >&2; }
+fail() { printf 'FAIL %s\n' "$*" >&2; FAILED=1; }
+
+if ! command -v kubectl >/dev/null 2>&1; then
+ echo "FAIL kubectl is not installed; install it and select the target context" >&2
+ exit 1
+fi
+
+if ! kubectl -n "$NS" get deployment "$GATEWAY_DEPLOY" >/dev/null 2>&1; then
+ echo "FAIL deploy/$GATEWAY_DEPLOY not found in namespace $NS." >&2
+ echo " Apply the base manifests first: kubectl apply -f manifests/" >&2
+ echo " Then apply the variant: kubectl apply -f manifests/exposure/gateway-api-sso/" >&2
+ exit 1
+fi
+
+echo "== Secrets"
+# check_secret_key
+check_secret_key() {
+ local secret=$1 key=$2 hint=$3 value escaped
+ # Kubernetes JSONPath treats "." as a path separator; escape it in keys such
+ # as tls.crt.
+ escaped=${key//./\\.}
+ value=$(kubectl -n "$NS" get secret "$secret" -o jsonpath="{.data.${escaped}}" 2>/dev/null || true)
+ if [ -n "$value" ]; then
+ pass "Secret $secret has non-empty key '$key'"
+ else
+ fail "Secret $secret is missing a non-empty '$key' key. $hint"
+ fi
+}
+
+check_secret_key "$OAUTH_SECRET" client-id \
+ "Create it from manifests/exposure/gateway-api-sso/73-oauth2-proxy-secret.yaml.example."
+check_secret_key "$OAUTH_SECRET" client-secret \
+ "Create it from manifests/exposure/gateway-api-sso/73-oauth2-proxy-secret.yaml.example."
+check_secret_key "$OAUTH_SECRET" cookie-secret \
+ "Create it from manifests/exposure/gateway-api-sso/73-oauth2-proxy-secret.yaml.example."
+
+# The cookie secret must be URL-safe unpadded base64 of 16, 24 or 32 bytes;
+# oauth2-proxy refuses anything else (the classic error is "44 bytes").
+if COOKIE_B64=$(kubectl -n "$NS" get secret "$OAUTH_SECRET" \
+ -o jsonpath='{.data.cookie-secret}' 2>/dev/null); then
+ if [ -n "$COOKIE_B64" ]; then
+ if ! COOKIE_LEN=$(printf '%s' "$COOKIE_B64" | base64 -d 2>/dev/null | wc -c); then
+ fail "Secret $OAUTH_SECRET key 'cookie-secret' is not valid base64. Generate it with: python3 -c 'import secrets; print(secrets.token_urlsafe(32))'"
+ else
+ case "$COOKIE_LEN" in
+ 16 | 24 | 32) pass "oauth2-proxy cookie-secret decodes to $COOKIE_LEN bytes" ;;
+ *)
+ fail "oauth2-proxy cookie-secret decodes to $COOKIE_LEN bytes; it must be 16, 24 or 32. Generate a URL-safe unpadded value with: python3 -c 'import secrets; print(secrets.token_urlsafe(32))'"
+ ;;
+ esac
+ fi
+ fi
+fi
+
+check_secret_key "$TLS_SECRET" tls.crt \
+ "Create the TLS Secret with: kubectl -n $NS create secret tls $TLS_SECRET --cert=tls.crt --key=tls.key"
+check_secret_key "$TLS_SECRET" tls.key \
+ "Create the TLS Secret with: kubectl -n $NS create secret tls $TLS_SECRET --cert=tls.crt --key=tls.key"
+
+echo
+echo "== Gateway SSO configuration (deploy/$GATEWAY_DEPLOY)"
+GATEWAY_ENV=$(kubectl -n "$NS" get deployment "$GATEWAY_DEPLOY" \
+ -o jsonpath='{range .spec.template.spec.containers[*].env[*]}{.name}={.value}{"\n"}{end}')
+
+# env_value : print the literal env value, or nothing when unset.
+env_value() {
+ printf '%s\n' "$GATEWAY_ENV" | awk -F= -v key="$1" '$1 == key { sub(/^[^=]*=/, ""); print }'
+}
+env_is_set() {
+ printf '%s\n' "$GATEWAY_ENV" | awk -F= -v key="$1" '$1 == key { found = 1 } END { exit found ? 0 : 1 }'
+}
+
+if ! env_is_set AUTH_PROXY_HEADER; then
+ fail "AUTH_PROXY_HEADER is not set on deploy/$GATEWAY_DEPLOY, so the gateway ignores oauth2-proxy's identity header and users are still asked for the gateway token. Fix: kubectl -n $NS set env deploy/$GATEWAY_DEPLOY AUTH_PROXY_HEADER=X-Forwarded-User"
+elif [ "$(env_value AUTH_PROXY_HEADER)" != "X-Forwarded-User" ]; then
+ fail "AUTH_PROXY_HEADER is '$(env_value AUTH_PROXY_HEADER)', expected 'X-Forwarded-User'. Fix: kubectl -n $NS set env deploy/$GATEWAY_DEPLOY AUTH_PROXY_HEADER=X-Forwarded-User"
+else
+ pass "AUTH_PROXY_HEADER=X-Forwarded-User"
+fi
+
+if ! env_is_set COOKIE_SECURE; then
+ fail "COOKIE_SECURE is not set on deploy/$GATEWAY_DEPLOY; the session cookie must be Secure behind TLS. Fix: kubectl -n $NS set env deploy/$GATEWAY_DEPLOY COOKIE_SECURE=true"
+elif [ "$(env_value COOKIE_SECURE | tr '[:upper:]' '[:lower:]')" != "true" ]; then
+ fail "COOKIE_SECURE is '$(env_value COOKIE_SECURE)', expected 'true' behind TLS. Fix: kubectl -n $NS set env deploy/$GATEWAY_DEPLOY COOKIE_SECURE=true"
+else
+ pass "COOKIE_SECURE=true"
+fi
+
+if env_is_set ALLOW_INSECURE_COOKIES; then
+ warn "ALLOW_INSECURE_COOKIES is still set on deploy/$GATEWAY_DEPLOY. It is ignored while COOKIE_SECURE=true but is a footgun: remove it with 'kubectl -n $NS set env deploy/$GATEWAY_DEPLOY ALLOW_INSECURE_COOKIES-'."
+fi
+
+echo
+echo "== SSO objects and placeholders"
+if kubectl -n "$NS" get deployment "$OAUTH_DEPLOY" >/dev/null 2>&1; then
+ pass "deploy/$OAUTH_DEPLOY exists"
+ OAUTH_ARGS=$(kubectl -n "$NS" get deployment "$OAUTH_DEPLOY" \
+ -o jsonpath='{range .spec.template.spec.containers[*].args[*]}{@}{"\n"}{end}')
+else
+ OAUTH_ARGS=""
+ fail "deploy/$OAUTH_DEPLOY not found. Apply the variant: kubectl apply -f manifests/exposure/gateway-api-sso/"
+fi
+
+# GitHub org restriction is the only authorization in the shipped variant.
+# --email-domain=* is acceptable only because --github-org narrows membership;
+# a missing or empty org value would accept any GitHub account.
+if [ -n "$OAUTH_ARGS" ]; then
+ if printf '%s\n' "$OAUTH_ARGS" | grep -q '^--provider=github$'; then
+ pass "oauth2-proxy uses --provider=github"
+ else
+ fail "oauth2-proxy is not configured with --provider=github. Fix: add --provider=github to deploy/$OAUTH_DEPLOY."
+ fi
+ if printf '%s\n' "$OAUTH_ARGS" | grep -Eq '^--github-org=.+$'; then
+ pass "oauth2-proxy restricts membership with --github-org"
+ else
+ fail "oauth2-proxy has no non-empty --github-org. Fix: set --github-org= in deploy/$OAUTH_DEPLOY; otherwise --email-domain=* accepts any GitHub account."
+ fi
+fi
+
+if kubectl -n "$NS" get httproute dsh-gateway >/dev/null 2>&1; then
+ pass "HTTPRoute dsh-gateway exists"
+ ROUTE_STATE=$(kubectl -n "$NS" get httproute dsh-gateway \
+ -o jsonpath='{.spec.hostnames[*]}' 2>/dev/null || true)
+else
+ ROUTE_STATE=""
+ fail "HTTPRoute dsh-gateway not found. Apply the variant: kubectl apply -f manifests/exposure/gateway-api-sso/"
+fi
+
+if kubectl -n "$NS" get gateway dsh-gateway >/dev/null 2>&1; then
+ pass "Gateway dsh-gateway exists"
+ GATEWAY_STATE=$(kubectl -n "$NS" get gateway dsh-gateway \
+ -o jsonpath='{range .spec.listeners[*]}{.hostname}{"\n"}{end}' 2>/dev/null || true)
+ CERT_REFS=$(kubectl -n "$NS" get gateway dsh-gateway \
+ -o jsonpath='{range .spec.listeners[*].tls.certificateRefs[*]}{.namespace}{"/"}{.name}{"\n"}{end}' 2>/dev/null || true)
+else
+ GATEWAY_STATE=""
+ CERT_REFS=""
+ fail "Gateway dsh-gateway not found. Apply the variant: kubectl apply -f manifests/exposure/gateway-api-sso/"
+fi
+
+# Placeholders are the default values in the committed manifests. They reach
+# the cluster if the variant is applied before editing them.
+PLACEHOLDER_RE='your-github-org|dsh-demo\.example\.com'
+PLACEHOLDER_STATE=$(printf '%s\n%s\n%s\n' "$OAUTH_ARGS" "$ROUTE_STATE" "$GATEWAY_STATE")
+if printf '%s\n' "$PLACEHOLDER_STATE" | grep -En "$PLACEHOLDER_RE" >/dev/null 2>&1; then
+ fail "the applied SSO objects still contain a placeholder (your-github-org or dsh-demo.example.com):"
+ printf '%s\n' "$PLACEHOLDER_STATE" | grep -En "$PLACEHOLDER_RE" | sed 's/^/ /' >&2
+ echo " Replace --github-org, --redirect-url, the Gateway listener host and the HTTPRoute hostname with the real public FQDN and organization, then re-apply and restart the proxy." >&2
+else
+ pass "no your-github-org / dsh-demo.example.com placeholder in the applied SSO objects"
+fi
+
+# The Gateway must reference an existing TLS Secret; the default name is
+# dsh-demo-tls, but 70-gateway.yaml can be edited to another name.
+if [ -n "$CERT_REFS" ]; then
+ while IFS= read -r ref; do
+ [ -n "$ref" ] || continue
+ ref_ns=${ref%%/*}
+ ref_name=${ref#*/}
+ ref_ns=${ref_ns:-$NS}
+ if kubectl -n "$ref_ns" get secret "$ref_name" >/dev/null 2>&1; then
+ pass "Gateway TLS certificate reference ${ref_ns}/${ref_name} exists"
+ else
+ fail "Gateway references TLS Secret ${ref_ns}/${ref_name} but it does not exist. Create it (see manifests/exposure/gateway-api-sso/74-tls-secret.yaml.example)."
+ fi
+ done <<<"$CERT_REFS"
+else
+ warn "the Gateway has no TLS certificateRefs; SSO over plain HTTP would expose the session cookie. Terminate TLS at the Gateway listener."
+fi
+
+# A second path to the gateway defeats SSO: anyone who can reach
+# dsh-gateway:8080 can spoof X-Forwarded-User. This is a best-effort check.
+if kubectl -n "$NS" get ingress >/dev/null 2>&1; then
+ INGRESS_STATE=$(kubectl -n "$NS" get ingress -o json 2>/dev/null || true)
+ if printf '%s' "$INGRESS_STATE" | grep -q '"name":"dsh-gateway"'; then
+ fail "an Ingress in namespace $NS also routes to dsh-gateway. With SSO enabled the gateway must be reachable only through oauth2-proxy: delete the Ingress variant (kubectl delete -f manifests/exposure/ingress/)."
+ fi
+fi
+
+echo
+if [ "$FAILED" -ne 0 ]; then
+ echo "preflight-sso: FAILED - fix the items above before exposing the gateway." >&2
+ exit 1
+fi
+echo "preflight-sso: ok - the SSO variant looks correctly configured."
diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh
index 002248d..8f0142a 100755
--- a/scripts/check-versions.sh
+++ b/scripts/check-versions.sh
@@ -22,14 +22,48 @@ expect() { #
# Main Dockerfile build args
expect Dockerfile "ARG DSH_VERSION=$DSH_VERSION"
+expect Dockerfile "ARG DSH_COMMIT=$DSH_COMMIT"
expect Dockerfile "ARG PNPM_VERSION=$PNPM_VERSION"
expect Dockerfile "ARG RELEASE_REVISION=$RELEASE_REVISION"
+expect Dockerfile "ARG NODE_BUILD_IMAGE=$NODE_BUILD_IMAGE"
+expect Dockerfile "ARG NODE_RUNTIME_IMAGE=$NODE_RUNTIME_IMAGE"
+# The build stage must fail closed when the tag no longer resolves to DSH_COMMIT.
+expect Dockerfile "git -C /src rev-parse HEAD"
+
+# Example gateway build base image
+expect examples/dsh-agents-as-a-service/gateway/Dockerfile \
+ "ARG GATEWAY_PYTHON_IMAGE=$GATEWAY_PYTHON_IMAGE"
# Local configuration mirrored for humans
expect .env.sample "DSH_VERSION=$DSH_VERSION"
+expect .env.sample "DSH_COMMIT=$DSH_COMMIT"
expect .env.sample "PNPM_VERSION=$PNPM_VERSION"
expect .env.sample "RELEASE_REVISION=$RELEASE_REVISION"
+# Human-facing docs embed the resolved commit in the build example and table.
+expect docs/main-image.md "$DSH_COMMIT"
+expect docs/versioning.md "$DSH_COMMIT"
+
+# Every container image is pinned to an immutable multi-arch index digest, not
+# a mutable tag. This is a supply-chain invariant: don't "fix" a failing check
+# by dropping the digest.
+for image_var in NODE_BUILD_IMAGE NODE_RUNTIME_IMAGE GATEWAY_PYTHON_IMAGE OAUTH2_PROXY_IMAGE; do
+ image_value="${!image_var:-}"
+ if ! printf '%s' "$image_value" | grep -qE '@sha256:[0-9a-f]{64}$'; then
+ err "versions.env: $image_var must be digest-pinned (name:tag@sha256:<64 hex>), got: ${image_value:-}"
+ fi
+done
+
+if ! printf '%s' "${DSH_COMMIT:-}" | grep -qE '^[0-9a-f]{40}$'; then
+ err "versions.env: DSH_COMMIT must be a full 40-character commit SHA, got: ${DSH_COMMIT:-}"
+fi
+
+# Build entrypoints must pass DSH_COMMIT rather than trusting the tag alone.
+expect Makefile "--build-arg DSH_COMMIT=\$(DSH_COMMIT)"
+for wf in build-main.yaml release.yaml ci-image.yaml; do
+ expect ".github/workflows/$wf" "DSH_COMMIT="
+done
+
# Image references in the example manifests
for f in \
examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml \
@@ -38,6 +72,10 @@ for f in \
done
expect examples/dsh-agents-as-a-service/manifests/50-gateway.yaml "image: $GATEWAY_IMAGE:$IDENTITY"
+# The oauth2-proxy exposure variant pins the digest, not the mutable tag.
+expect examples/dsh-agents-as-a-service/manifests/exposure/gateway-api-sso/72-oauth2-proxy.yaml \
+ "image: $OAUTH2_PROXY_IMAGE"
+
# The gateway image must live below the repository image (one package per
# example component, no unrelated top-level packages).
case "$GATEWAY_IMAGE" in
diff --git a/scripts/set-version.sh b/scripts/set-version.sh
index e8ff652..7dc47b5 100755
--- a/scripts/set-version.sh
+++ b/scripts/set-version.sh
@@ -4,6 +4,11 @@
#
# ./scripts/set-version.sh dsh-v0.2.2-rc.1 # new DSH ref, no revision
# ./scripts/set-version.sh dsh-v0.2.1-rc.5 r3 # same ref, new repo revision
+#
+# For a new DSH ref the script resolves the tag's commit with "git ls-remote"
+# (peeled ^{} commit first, tag ref as fallback) and rewrites DSH_COMMIT in
+# versions.env, the Dockerfile ARG default and .env.sample. A revision-only bump
+# at the same ref keeps the existing commit and needs no network access.
set -euo pipefail
cd "$(dirname "$0")/.."
@@ -23,9 +28,11 @@ if [ -n "$new_revision" ]; then
fi
# shellcheck source=/dev/null
-old_version="$(. ./versions.env; printf '%s' "$DSH_VERSION")"
-# shellcheck source=/dev/null
-old_revision="$(. ./versions.env; printf '%s' "${RELEASE_REVISION:-}")"
+. ./versions.env
+
+old_version="$DSH_VERSION"
+old_revision="${RELEASE_REVISION:-}"
+old_commit="${DSH_COMMIT:-}"
old_identity="$old_version"
[ -z "$old_revision" ] || old_identity="$old_version-$old_revision"
@@ -37,6 +44,29 @@ if [ "$old_identity" = "$new_identity" ]; then
exit 0
fi
+# Resolve the commit the new upstream tag points at. Annotated tags expose a
+# peeled ^{} line (the commit) and a tag-object line; prefer the peeled commit
+# and fall back to the tag ref for lightweight tags. A repository-only revision
+# bump keeps the already pinned commit, so it needs no network.
+new_commit="$old_commit"
+if [ "$new_version" != "$old_version" ] || [ -z "$new_commit" ]; then
+ if ! refs="$(git ls-remote --tags --exit-code "$DSH_REPO" \
+ "refs/tags/$new_version^{}" "refs/tags/$new_version")"; then
+ echo "set-version: upstream tag '$new_version' not found in $DSH_REPO" >&2
+ exit 1
+ fi
+ new_commit="$(printf '%s\n' "$refs" \
+ | awk -v t="refs/tags/$new_version^{}" '$2 == t {print $1}' | head -n1)"
+ if [ -z "$new_commit" ]; then
+ new_commit="$(printf '%s\n' "$refs" \
+ | awk -v t="refs/tags/$new_version" '$2 == t {print $1}' | head -n1)"
+ fi
+ if ! printf '%s' "$new_commit" | grep -qE '^[0-9a-f]{40}$'; then
+ echo "set-version: could not resolve a commit for '$new_version' in $DSH_REPO" >&2
+ exit 1
+ fi
+fi
+
# Replace the identity first (it contains the base version), then the base
# version itself. CHANGELOG.md is generated from tags, so it is left alone.
while IFS= read -r file; do
@@ -48,17 +78,30 @@ while IFS= read -r file; do
sed -i "s/$old_identity/$new_identity/g; s/$old_version/$new_version/g" "$file"
done < <(git ls-files)
-# versions.env is authoritative: write both keys explicitly so an empty
+# versions.env is authoritative: write these keys explicitly so an empty
# revision is stored as an empty value.
sed -i "s/^DSH_VERSION=.*/DSH_VERSION=$new_version/" versions.env
sed -i "s/^RELEASE_REVISION=.*/RELEASE_REVISION=$new_revision/" versions.env
+sed -i "s/^DSH_COMMIT=.*/DSH_COMMIT=$new_commit/" versions.env
# Values stored separately from the full identity: Dockerfile ARG defaults and
# the human-facing sample cannot be reached by the identity replacement above.
sed -i "s/^ARG DSH_VERSION=.*/ARG DSH_VERSION=$new_version/" Dockerfile
sed -i "s/^ARG RELEASE_REVISION=.*/ARG RELEASE_REVISION=$new_revision/" Dockerfile
+sed -i "s/^ARG DSH_COMMIT=.*/ARG DSH_COMMIT=$new_commit/" Dockerfile
sed -i "s/^DSH_VERSION=.*/DSH_VERSION=$new_version/" .env.sample
sed -i "s/^RELEASE_REVISION=.*/RELEASE_REVISION=$new_revision/" .env.sample
+sed -i "s/^DSH_COMMIT=.*/DSH_COMMIT=$new_commit/" .env.sample
+
+# The human-facing build examples embed the full SHA, which the version
+# replacement above cannot see. Keep them in step; check-versions.sh verifies.
+if [ -n "$old_commit" ] && [ "$old_commit" != "$new_commit" ]; then
+ for doc in docs/main-image.md docs/versioning.md; do
+ [ -f "$doc" ] || continue
+ sed -i "s/$old_commit/$new_commit/g" "$doc"
+ done
+fi
echo "set-version: $old_identity -> $new_identity"
+echo "set-version: DSH_COMMIT=$new_commit"
echo "set-version: run 'make check' and review the diff"
diff --git a/versions.env b/versions.env
index 68f1004..0548461 100644
--- a/versions.env
+++ b/versions.env
@@ -2,10 +2,17 @@
# repository. Consumed by the Makefile, build.sh, scripts/*.sh and the GitHub
# Actions workflows. Never hard-code these values elsewhere: run "make check"
# (scripts/check-versions.sh) to verify every pin.
+#
+# Every container image below is pinned to an immutable multi-arch index digest
+# (name:tag@sha256:...), not just a mutable tag. Bump the tag and the digest
+# together; scripts/check-versions.sh requires the digest form.
# Upstream DeepSeek Harness source, and the ref this repository packages.
+# DSH_COMMIT is the full commit SHA that DSH_VERSION resolves to upstream; the
+# Dockerfile verifies the clone's HEAD against it and fails the build on drift.
DSH_REPO=https://github.com/deepseek-ai/deepseek-harness.git
DSH_VERSION=dsh-v0.2.1-rc.5
+DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1
PNPM_VERSION=11.28.5
# Release identity = DSH_VERSION plus an optional repository revision.
@@ -15,6 +22,13 @@ PNPM_VERSION=11.28.5
# repository-only change at the same ref becomes -r2.
RELEASE_REVISION=
+# Base images for the main DSH Web image (root Dockerfile), pinned by digest.
+NODE_BUILD_IMAGE=docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19
+NODE_RUNTIME_IMAGE=docker.io/node:24-trixie-slim@sha256:54f84b4cf8432b21aaffd5d3ce8d41e12cea0d10d33c09d2f3c9879d49d6b446
+
+# Base image for the example gateway (examples/dsh-agents-as-a-service/gateway).
+GATEWAY_PYTHON_IMAGE=docker.io/python:3.12-slim@sha256:aa943ce411423c7582288f18707ff66eddcbaeb510502d747a72aeb34f30d159
+
# Control-plane versions used by the examples and their manifests.
# The matching SHA-256s pin the release manifests that CI downloads and
# applies; update both the version and the digest when bumping either pin.
@@ -22,7 +36,7 @@ AGENT_SANDBOX_VERSION=v1.0.9
AGENT_SANDBOX_MANIFEST_SHA256=4bf9ee5558e9a663f91cfa5963e96f2591bfaa1dcf79f03bfab65bc5d65cc699
GATEWAY_API_VERSION=v1.2.1
GATEWAY_API_MANIFEST_SHA256=97598bf6ab3b33b9b5c5432bdd24de091e4e9c3aa0575ebb0710a2a19cd64d64
-OAUTH2_PROXY_IMAGE=quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
+OAUTH2_PROXY_IMAGE=quay.io/oauth2-proxy/oauth2-proxy:v7.15.5@sha256:8498b0d0ef0a7b29686414000a08aee467f02d0299c9ed1e006a8f33fc017916
# Registry layout: the main image is the repository itself, and every example
# component is a nested package below it (////).
From 8e9bd478b4bf47a196cbdc4da68d6de407f611a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Georger?=
Date: Sat, 31 Oct 2026 06:25:39 -0300
Subject: [PATCH 2/3] =?UTF-8?q?chore:=20improve=20package=20installation?=
=?UTF-8?q?=20and=20add=20Python=20depend=E2=80=A6=20(#42)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* chore(dockerfile): improve package installation and add Python dependencies
* chore(dockerfile): update package installation commands for consistency
* chore: update release version to dsh-v0.2.1-rc.5-r6 across documentation and configuration files
---
.env.sample | 2 +-
AGENTS.md | 2 +-
CHANGELOG.md | 34 +++++++++++++++++++
Dockerfile | 9 +++--
README.md | 12 +++----
docs/README.md | 2 +-
docs/main-image.md | 10 +++---
docs/versioning.md | 12 +++----
examples/dsh-agents-as-a-service/README.md | 2 +-
.../dsh-agents-as-a-service/gateway/README.md | 2 +-
.../manifests/10-sandboxtemplate.yaml | 2 +-
.../manifests/50-gateway.yaml | 2 +-
.../manifests/10-sandboxtemplate.yaml | 2 +-
scripts/set-version.sh | 2 +-
versions.env | 4 +--
15 files changed, 68 insertions(+), 31 deletions(-)
diff --git a/.env.sample b/.env.sample
index c74e00c..05c8ef4 100644
--- a/.env.sample
+++ b/.env.sample
@@ -26,7 +26,7 @@ DSH_VERSION=dsh-v0.2.1-rc.5
# Full upstream commit SHA that DSH_VERSION resolves to; the main image build
# fails if the clone's HEAD differs (guards against a moved tag).
DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1
-RELEASE_REVISION=
+RELEASE_REVISION=r6
PNPM_VERSION=11.28.5
# Container port, and the host port published by "make run".
DSH_PORT=3080
diff --git a/AGENTS.md b/AGENTS.md
index 7cb1ed9..c389f41 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -82,7 +82,7 @@ needs kind plus the agent-sandbox CRDs (see the example README).
## Versioning and releases
- Release identity = DSH_VERSION plus an optional -rN repository revision.
- The current identity is dsh-v0.2.1-rc.5 (first release at this DSH ref, so no
+ The current identity is dsh-v0.2.1-rc.5-r6 (first release at this DSH ref, so no
suffix); a repository-only change at the same ref publishes -r2.
- Channels: release tags are immutable; latest follows the newest release;
main is the development channel for both image packages.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e73d2e..884e4c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,40 @@
All notable changes to this repository are documented here.
Generated by git-cliff from Conventional Commits; do not edit this file by hand.
+## [Unreleased]
+
+### Added
+
+- **example:** Persist the agent workspace on the PVC via subPath (#34) (367294b)
+- **example:** Start DSH's folder picker on the persisted workspace (#36) (82ee0d0)
+
+### CI
+
+- **workflows:** Streamline CI and remove redundant builds (#31) (dcb5b43)
+
+### Documentation
+
+- Align readmes, docs and agent guidance with current behavior (#37) (24f23e0)
+
+### Fixed
+
+- **gateway:** Answer client-aborted requests with 499, not a traceback (#32) (cbcc56f)
+- Ship bubblewrap for DSH's command sandbox, keep agents usable without it (#35) (a09d4ac)
+- **ci:** Harden workflow token scope and download integrity (#39) (2f959e6)
+- **gateway:** Replace token cookies with revocable sessions and harden abuse controls (#40) (1a929e3)
+
+### Maintenance
+
+- **changelog:** Refresh for dsh-v0.2.1-rc.5 (#30) (b57916d)
+- Bump docker/login-action (#28) (8c34f43)
+- **deps:** Bump agent-sandbox to v1.0.9 (#38) (4c073ee)
+- **dockerfile:** Improve package installation and add Python dependencies (bd7c6f3)
+- **dockerfile:** Update package installation commands for consistency (0a3fe25)
+
+### Other
+
+- Add continue-on-error to CHANGELOG PR step (97c5632)
+
## [dsh-v0.2.1-rc.5] - 2026-10-30
### Added
diff --git a/Dockerfile b/Dockerfile
index a28e2d3..700d5d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -68,7 +68,7 @@ RUN cd /opt/dsh \
FROM $NODE_RUNTIME_IMAGE AS runtime
ARG DSH_VERSION=dsh-v0.2.1-rc.5
-ARG RELEASE_REVISION=
+ARG RELEASE_REVISION=r6
ARG PNPM_VERSION=11.28.5
ARG VCS_REF=unknown
@@ -106,8 +106,11 @@ RUN apt-get update \
tini \
gh \
bubblewrap \
+ python3 \
+ python3-pip \
+ python3-venv \
&& rm -rf /var/lib/apt/lists/* \
- && curl -LsSf https://astral.sh/uv/install.sh | sh \
+ && apt-get autoclean -y \
&& install -d -o node -g node -m 0755 /workspace "$DSH_HOME" \
&& corepack enable \
&& corepack prepare "pnpm@$PNPM_VERSION" --activate \
@@ -160,6 +163,6 @@ WORKDIR /workspace
USER node
EXPOSE 3080
-VOLUME ["/home/node/.dsh"]
+VOLUME ["/home/node/.dsh", "/workspace"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/dsh-entrypoint"]
diff --git a/README.md b/README.md
index 97e8879..446df62 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ docker run -d --name dsh-web \
-p 3080:3080 \
-v dsh-home:/home/node/.dsh \
-e DEEPSEEK_API_KEY \
- ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+ ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
```
DSH Web is **not usable at a bare `/`**. Without its launch-token cookie it
@@ -45,7 +45,7 @@ docker run -d --name dsh-web -p 18081:3080 \
-e DSH_PUBLIC_URL=http://localhost:18081 \
-v dsh-home:/home/node/.dsh \
-e DEEPSEEK_API_KEY \
- ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+ ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
# then open http://localhost:18081/?token=...
```
@@ -94,20 +94,20 @@ ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway
| Tag | Contents | Meaning |
| --- | --- | --- |
-| `dsh-v0.2.1-rc.5` | DSH Web, port `3080` | **Current release identity.** Immutable: once published it never moves. |
+| `dsh-v0.2.1-rc.5-r6` | DSH Web, port `3080` | **Current release identity.** Immutable: once published it never moves. |
| `latest` | DSH Web | Most recent release; same digest as the current identity. |
| `main` | DSH Web | Development channel rebuilt when image-definition files change on `main`. Not a release; not for production. |
The example gateway is published under its nested path with the same release
identity, e.g.
-`ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5`.
+`ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6`.
Release identities, channels and rollback rules are explained in
[docs/versioning.md](docs/versioning.md). Production deployments should pin a
digest:
```bash
-docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:
```
@@ -188,7 +188,7 @@ required checks and releases: [docs/gitflow.md](docs/gitflow.md).
The release identity is the upstream DSH ref plus an optional repository
revision: `DSH_VERSION` and `RELEASE_REVISION` in [versions.env](versions.env).
-The current identity is **`dsh-v0.2.1-rc.5`** — the first release at this
+The current identity is **`dsh-v0.2.1-rc.5-r6`** — the first release at this
upstream DSH ref, so it carries no repository revision. Tags are immutable; a
broken release is fixed by publishing `-r(N+1)`, never by re-tagging.
diff --git a/docs/README.md b/docs/README.md
index 192ec1c..45f8c02 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,7 +14,7 @@ gets a DSH Web container running before anything else.
| [main-image.md](main-image.md) | The primary artifact: build args, runtime environment, entrypoint, probes, volumes/ports/signals, non-root contract, image names, tags and digests. |
| [examples.md](examples.md) | The example contract: required layout, manifest rules, exposure variants, components and image naming, scripts, docs, and how to add a new example. |
| [gitflow.md](gitflow.md) | Branches, protected `main`, squash merges, Conventional PR titles, required CI checks, release branches, hotfixes and the one-time GitHub settings checklist. |
-| [versioning.md](versioning.md) | Release identity (`DSH ref` + optional `-rN`), the current identity `dsh-v0.2.1-rc.5`, why identities are immutable, channels, compatibility matrix, bumping pins, the release checklist, rollback, git-cliff and digest pinning. |
+| [versioning.md](versioning.md) | Release identity (`DSH ref` + optional `-rN`), the current identity `dsh-v0.2.1-rc.5-r6`, why identities are immutable, channels, compatibility matrix, bumping pins, the release checklist, rollback, git-cliff and digest pinning. |
| [workflow-security.md](workflow-security.md) | GitHub Actions security posture: least-privilege tokens, pinned actions, untrusted-input handling, download integrity checks, artifact hygiene, the latest audit record and residual repository-setting recommendations. |
## Project skills
diff --git a/docs/main-image.md b/docs/main-image.md
index 8e21636..0095da6 100644
--- a/docs/main-image.md
+++ b/docs/main-image.md
@@ -39,12 +39,12 @@ To reproduce the equivalent by hand:
```bash
docker build \
- --build-arg DSH_VERSION=dsh-v0.2.1-rc.5 \
+ --build-arg DSH_VERSION=dsh-v0.2.1-rc.5-r6 \
--build-arg DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1 \
--build-arg RELEASE_REVISION= \
--build-arg PNPM_VERSION=11.28.5 \
--build-arg VCS_REF="$(git rev-parse --short HEAD)" \
- -t ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5 .
+ -t ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6 .
```
### Build args
@@ -209,7 +209,7 @@ GitHub Container Registry:
| Tag | Meaning |
| --- | --- |
-| `dsh-v0.2.1-rc.5` | Current release identity. **Immutable**: once published it never moves. |
+| `dsh-v0.2.1-rc.5-r6` | Current release identity. **Immutable**: once published it never moves. |
| `latest` | Most recent release; points at the same digest as the current identity. |
| `main` | Development channel rebuilt when image-definition files change on `main`; not a release, not for production. |
@@ -218,7 +218,7 @@ Release identities, channels and rollback rules are defined in
pin a digest:
```bash
-docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
# ... pin the digest in your deployment manifests:
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:
```
@@ -226,7 +226,7 @@ docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbo
## Verifying an image
```bash
-docker inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5 \
+docker inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6 \
--format '{{json .Config.Labels}}'
# org.opencontainers.image.title / .version / .source
# io.sredevopsorg.dsh.version / io.sredevopsorg.release.revision
diff --git a/docs/versioning.md b/docs/versioning.md
index 66b6a17..475ab4c 100644
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -15,12 +15,12 @@ release identity = DSH_VERSION + optional "-" + RELEASE_REVISION
| Piece | Meaning |
| --- | --- |
-| `DSH_VERSION` | The upstream DeepSeek Harness git ref being packaged, e.g. `dsh-v0.2.1-rc.5`. It mirrors upstream's own tag. |
+| `DSH_VERSION` | The upstream DeepSeek Harness git ref being packaged, e.g. `dsh-v0.2.1-rc.5-r6`. It mirrors upstream's own tag. |
| `RELEASE_REVISION` | An optional repository revision at that DSH ref: empty for the first release, `rN` (`N >= 2`) for a repository-only re-release. |
Examples:
-- `DSH_VERSION=dsh-v0.2.1-rc.5`, `RELEASE_REVISION=` → identity `dsh-v0.2.1-rc.5`
+- `DSH_VERSION=dsh-v0.2.1-rc.5-r6`, `RELEASE_REVISION=` → identity `dsh-v0.2.1-rc.5-r6`
(first release at that upstream ref).
- `DSH_VERSION=`, `RELEASE_REVISION=rN` → identity `-rN` (for example
documentation, CI or gateway changes on top of an already-released ref; no DSH
@@ -36,7 +36,7 @@ with an empty revision); a repository-only change at the same DSH ref increments
change what an existing deployment runs. A fix therefore publishes a new identity;
see the rules above.
-The current identity is `dsh-v0.2.1-rc.5`, the first release at this upstream
+The current identity is `dsh-v0.2.1-rc.5-r6`, the first release at this upstream
ref, so it carries no revision suffix. The identity is stored in
[versions.env](../versions.env) and consumed everywhere: the Dockerfile labels
(version and revision), the example manifests, the Makefile and the release
@@ -46,7 +46,7 @@ workflow.
| Channel | Image tag | Meaning |
| --- | --- | --- |
-| Release | `dsh-v0.2.1-rc.5` | The current immutable release identity. What production should pin (preferably by digest). |
+| Release | `dsh-v0.2.1-rc.5-r6` | The current immutable release identity. What production should pin (preferably by digest). |
| `latest` | `latest` | Alias for the most recent release; moves only when a new release is published. |
| `main` | `main` | Development channel, rebuilt when image or gateway sources change on `main`. Never treat it as a release; it exists so `main` can be tested. |
@@ -60,7 +60,7 @@ Values from [versions.env](../versions.env) at the current identity:
| Component | Pin | Source |
| --- | --- | --- |
| Upstream DSH source | `DSH_REPO` | [versions.env](../versions.env) |
-| DSH ref | `dsh-v0.2.1-rc.5` | `DSH_VERSION` |
+| DSH ref | `dsh-v0.2.1-rc.5-r6` | `DSH_VERSION` |
| DSH commit | `b830ec950faba0a46dda564be3e2d854afed94f1` | `DSH_COMMIT`; the main image build fails unless the clone's `HEAD` matches |
| Repository revision | `(empty)` | `RELEASE_REVISION` |
| pnpm | `11.28.5` | `PNPM_VERSION` |
@@ -176,7 +176,7 @@ Release identities are immutable in theory; deployments should still pin the
_resolved_ digest for reproducibility and supply-chain safety:
```bash
-docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
# then, in the deployment:
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:
```
diff --git a/examples/dsh-agents-as-a-service/README.md b/examples/dsh-agents-as-a-service/README.md
index c6105a4..48546c8 100644
--- a/examples/dsh-agents-as-a-service/README.md
+++ b/examples/dsh-agents-as-a-service/README.md
@@ -270,7 +270,7 @@ kubectl delete -f manifests/40-claim-rejected.yaml
```bash
# Published image (on kind, load it into the cluster; on a real cluster the
# kubelet pulls it from GHCR):
-GW=ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5
+GW=ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6
docker pull "$GW"
kind load docker-image "$GW" --name
diff --git a/examples/dsh-agents-as-a-service/gateway/README.md b/examples/dsh-agents-as-a-service/gateway/README.md
index 043f1ba..6dfb84d 100644
--- a/examples/dsh-agents-as-a-service/gateway/README.md
+++ b/examples/dsh-agents-as-a-service/gateway/README.md
@@ -24,7 +24,7 @@ It does four things:
The DSH version pin lives in [`versions.env`](../../../versions.env)
(`DSH_VERSION` plus the optional repository `RELEASE_REVISION`); the example
manifests and published images use the release identity built from it (currently
-`dsh-v0.2.1-rc.5`). DSH binds `:3080`, gates the UI and `/api` behind a
+`dsh-v0.2.1-rc.5-r6`). DSH binds `:3080`, gates the UI and `/api` behind a
`dsh-auth-*` cookie and requires a WebSocket at `/api/remote.mux`.
## Flow
diff --git a/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml b/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
index 432298b..9315740 100644
--- a/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/10-sandboxtemplate.yaml
@@ -73,7 +73,7 @@ spec:
fsGroupChangePolicy: OnRootMismatch
containers:
- name: dsh-web
- image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+ image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
imagePullPolicy: IfNotPresent
ports:
- name: web
diff --git a/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml b/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
index f5e51cb..024e52e 100644
--- a/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
+++ b/examples/dsh-agents-as-a-service/manifests/50-gateway.yaml
@@ -101,7 +101,7 @@ spec:
type: RuntimeDefault
containers:
- name: gateway
- image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5
+ image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
diff --git a/examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml b/examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml
index 482ddc8..e118d84 100644
--- a/examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml
+++ b/examples/minimal-single-tenant/manifests/10-sandboxtemplate.yaml
@@ -56,7 +56,7 @@ spec:
fsGroupChangePolicy: OnRootMismatch
containers:
- name: dsh-web
- image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
+ image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
imagePullPolicy: IfNotPresent
ports:
- name: web
diff --git a/scripts/set-version.sh b/scripts/set-version.sh
index 7dc47b5..2c9f268 100755
--- a/scripts/set-version.sh
+++ b/scripts/set-version.sh
@@ -3,7 +3,7 @@
# "make check".
#
# ./scripts/set-version.sh dsh-v0.2.2-rc.1 # new DSH ref, no revision
-# ./scripts/set-version.sh dsh-v0.2.1-rc.5 r3 # same ref, new repo revision
+# ./scripts/set-version.sh dsh-v0.2.1-rc.5-r6 r3 # same ref, new repo revision
#
# For a new DSH ref the script resolves the tag's commit with "git ls-remote"
# (peeled ^{} commit first, tag ref as fallback) and rewrites DSH_COMMIT in
diff --git a/versions.env b/versions.env
index 0548461..87826a9 100644
--- a/versions.env
+++ b/versions.env
@@ -18,9 +18,9 @@ PNPM_VERSION=11.28.5
# Release identity = DSH_VERSION plus an optional repository revision.
# An empty RELEASE_REVISION is the first release at this DSH ref; rN (N >= 2)
# marks a repository-only re-release at the same DSH ref. This is the first
-# release at dsh-v0.2.1-rc.5, so the identity is the plain tag; a later
+# release at dsh-v0.2.1-rc.5-r6, so the identity is the plain tag; a later
# repository-only change at the same ref becomes -r2.
-RELEASE_REVISION=
+RELEASE_REVISION=r6
# Base images for the main DSH Web image (root Dockerfile), pinned by digest.
NODE_BUILD_IMAGE=docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19
From 96837a43b61e3b6d359fcb6cd13bf4a3fece2240 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 31 Oct 2026 09:31:08 +0000
Subject: [PATCH 3/3] chore(changelog): refresh for dsh-v0.2.1-rc.5-r6
---
CHANGELOG.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 884e4c8..cd9eb2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
All notable changes to this repository are documented here.
Generated by git-cliff from Conventional Commits; do not edit this file by hand.
-## [Unreleased]
+## [dsh-v0.2.1-rc.5-r6] - 2026-10-31
### Added
@@ -30,8 +30,7 @@ Generated by git-cliff from Conventional Commits; do not edit this file by hand.
- **changelog:** Refresh for dsh-v0.2.1-rc.5 (#30) (b57916d)
- Bump docker/login-action (#28) (8c34f43)
- **deps:** Bump agent-sandbox to v1.0.9 (#38) (4c073ee)
-- **dockerfile:** Improve package installation and add Python dependencies (bd7c6f3)
-- **dockerfile:** Update package installation commands for consistency (0a3fe25)
+- Improve package installation and add Python depend… (#42) (8e9bd47)
### Other