From 282076359ae61eac3901098dfa8f61e299df332b Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Wed, 8 Jul 2026 16:26:15 -0700 Subject: [PATCH 1/5] feat(build): add Dagger CLI binary builds Add a rust-native-build Dagger function that builds and stages Linux amd64 and arm64 CLI binaries using the existing CI toolchain and workflow-equivalent build steps. Signed-off-by: Kris Hicks --- .dagger/.gitattributes | 1 + .dagger/.gitignore | 4 + .dagger/pyproject.toml | 12 + .dagger/src/open_shell/__init__.py | 16 + .dagger/src/open_shell/main.py | 244 +++++++++++ .dagger/tests/test_main.py | 37 ++ .dagger/uv.lock | 643 +++++++++++++++++++++++++++++ .daggerignore | 13 + dagger.json | 8 + flake.nix | 6 + nix/dagger.nix | 52 +++ 11 files changed, 1036 insertions(+) create mode 100644 .dagger/.gitattributes create mode 100644 .dagger/.gitignore create mode 100644 .dagger/pyproject.toml create mode 100644 .dagger/src/open_shell/__init__.py create mode 100644 .dagger/src/open_shell/main.py create mode 100644 .dagger/tests/test_main.py create mode 100644 .dagger/uv.lock create mode 100644 .daggerignore create mode 100644 dagger.json create mode 100644 nix/dagger.nix diff --git a/.dagger/.gitattributes b/.dagger/.gitattributes new file mode 100644 index 0000000000..827418463f --- /dev/null +++ b/.dagger/.gitattributes @@ -0,0 +1 @@ +/sdk/** linguist-generated diff --git a/.dagger/.gitignore b/.dagger/.gitignore new file mode 100644 index 0000000000..2343dfc87c --- /dev/null +++ b/.dagger/.gitignore @@ -0,0 +1,4 @@ +/.venv +/**/__pycache__ +/sdk +/.env diff --git a/.dagger/pyproject.toml b/.dagger/pyproject.toml new file mode 100644 index 0000000000..6b4b5f0d5c --- /dev/null +++ b/.dagger/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "open-shell" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = ["dagger-io"] + +[build-system] +requires = ["uv_build>=0.8.4,<0.9.0"] +build-backend = "uv_build" + +[tool.uv.sources] +dagger-io = { path = "sdk", editable = true } diff --git a/.dagger/src/open_shell/__init__.py b/.dagger/src/open_shell/__init__.py new file mode 100644 index 0000000000..5758bd7a9c --- /dev/null +++ b/.dagger/src/open_shell/__init__.py @@ -0,0 +1,16 @@ +"""A generated module for OpenShell functions + +This module has been generated via dagger init and serves as a reference to +basic module structure as you get started with Dagger. + +Two functions have been pre-created. You can modify, delete, or add to them, +as needed. They demonstrate usage of arguments and return types using simple +echo and grep commands. The functions can be called from the dagger CLI or +from one of the SDKs. + +The first line in this comment block is a short description line and the +rest is a long description with more detail on the module's purpose or usage, +if appropriate. All modules should have a short description. +""" + +from .main import OpenShell as OpenShell diff --git a/.dagger/src/open_shell/main.py b/.dagger/src/open_shell/main.py new file mode 100644 index 0000000000..240662f138 --- /dev/null +++ b/.dagger/src/open_shell/main.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import re +from dataclasses import dataclass + +import dagger +from dagger import dag, function, object_type + +DEFAULT_TOOLCHAIN_IMAGE = "ghcr.io/nvidia/openshell/ci:latest" +WORKDIR = "/src" +# Workflow parity: rust-native-build.yml includes the Zig-wrapper hash in its +# Rust cache key. Bump this whenever the Dagger copy of that wrapper changes. +RUST_TARGET_CACHE_GENERATION = "zig-musl-wrapper-v1" + +RUST_BUILD_SOURCE_INCLUDE = [ + ".cargo/", + "Cargo.lock", + "Cargo.toml", + "crates/", + "mise.lock", + "mise.toml", + "providers/", + "proto/", + "rust-toolchain.toml", +] + +_CARGO_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$") +_FEATURES_RE = re.compile(r"^[0-9A-Za-z_, -]*$") + + +@dataclass(frozen=True) +class RustBuildSpec: + component: str + arch: str + platform: dagger.Platform + crate: str + binary: str + rust_target: str + zig_target: str + + +def _rust_build_spec(component: str, arch: str) -> RustBuildSpec: + # Workflow parity: "Resolve build target" in rust-native-build.yml. + if component != "cli": + raise ValueError( + "component must be 'cli'; sandbox and gateway will be added incrementally" + ) + + match arch: + case "amd64": + platform = dagger.Platform("linux/amd64") + rust_target = "x86_64-unknown-linux-musl" + zig_target = "x86_64-linux-musl" + case "arm64": + platform = dagger.Platform("linux/arm64") + rust_target = "aarch64-unknown-linux-musl" + zig_target = "aarch64-linux-musl" + case _: + raise ValueError("arch must be one of: amd64, arm64") + + return RustBuildSpec( + component=component, + arch=arch, + platform=platform, + crate="openshell-cli", + binary="openshell", + rust_target=rust_target, + zig_target=zig_target, + ) + + +def _validate_inputs(cargo_version: str, features: str) -> None: + if cargo_version and not _CARGO_VERSION_RE.fullmatch(cargo_version): + raise ValueError("cargo-version must be a valid Cargo package version") + if not _FEATURES_RE.fullmatch(features): + raise ValueError("features contains unsupported characters") + + +@object_type +class OpenShell: + """Build reproducible OpenShell artifacts.""" + + @function + def rust_native_build( + self, + source_path: str = ".", + component: str = "cli", + arch: str = "amd64", + cargo_version: str = "", + image_tag: str = "dagger", + features: str = "bundled-z3", + toolchain_image: str = DEFAULT_TOOLCHAIN_IMAGE, + github_username: str = "", + github_token: dagger.Secret | None = None, + ) -> dagger.Directory: + """Build one native Linux Rust artifact; currently supports the CLI.""" + spec = _rust_build_spec(component, arch) + _validate_inputs(cargo_version, features) + + project_source = dag.current_workspace().directory( + source_path, + include=RUST_BUILD_SOURCE_INCLUDE, + gitignore=True, + ) + base = dag.container(platform=spec.platform) + if github_token is not None: + if not github_username: + raise ValueError( + "github-username is required when github-token is provided" + ) + base = base.with_registry_auth("ghcr.io", github_username, github_token) + + container = ( + base.from_(toolchain_image) + .with_directory(WORKDIR, project_source) + .with_workdir(WORKDIR) + # Workflow parity: "Cache Rust target and registry". + .with_mounted_cache( + "/root/.cargo/registry", + dag.cache_volume(f"openshell-cargo-registry-{spec.arch}"), + ) + .with_mounted_cache( + "/root/.cargo/git", + dag.cache_volume(f"openshell-cargo-git-{spec.arch}"), + ) + .with_mounted_cache( + f"{WORKDIR}/target", + dag.cache_volume( + "openshell-cargo-target-" + f"{spec.component}-{spec.arch}-{RUST_TARGET_CACHE_GENERATION}" + ), + ) + .with_mounted_cache( + f"{WORKDIR}/.cache/sccache", + dag.cache_volume(f"openshell-sccache-{spec.component}-{spec.arch}"), + ) + .with_env_variable("CARGO_INCREMENTAL", "0") + .with_env_variable("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1") + .with_env_variable("OPENSHELL_IMAGE_TAG", image_tag) + .with_env_variable("RUST_BUILD_ARCH", spec.arch) + .with_env_variable("RUST_BUILD_BINARY", spec.binary) + .with_env_variable("RUST_BUILD_CRATE", spec.crate) + .with_env_variable("RUST_BUILD_FEATURES", features) + .with_env_variable("RUST_BUILD_TARGET", spec.rust_target) + .with_env_variable("RUST_BUILD_ZIG_TARGET", spec.zig_target) + ) + + if github_token is not None: + container = container.with_secret_variable( + "MISE_GITHUB_TOKEN", github_token + ) + + # Workflow parity: "Install tools". Trust is Dagger-specific because + # the repository is mounted at /src instead of Actions' checkout path. + container = container.with_exec( + ["mise", "trust", f"{WORKDIR}/mise.toml"] + ).with_exec(["mise", "install", "--locked"]) + + if cargo_version: + # Workflow parity: "Patch workspace version". + container = ( + container.with_env_variable("OPENSHELL_CARGO_VERSION", cargo_version) + .with_env_variable("GIT_DIR", "/nonexistent") + .with_exec( + [ + "bash", + "-ec", + "sed -i -E " + "'/^\\[workspace\\.package\\]/,/^\\[/{" + 's/^version[[:space:]]*=[[:space:]]*".*"/' + 'version = "\'"$OPENSHELL_CARGO_VERSION"\'"/}\' ' + "Cargo.toml", + ] + ) + ) + + build_script = r""" +set -euo pipefail + +# Workflow parity: target installation from "Build ()". +mise x -- rustup target add "$RUST_BUILD_TARGET" + +# Workflow parity: "Set up zig musl wrappers". +ZIG="$(mise which zig)" +mkdir -p /tmp/zig-musl + +# cc-rs injects a Rust target triple that Zig does not parse, so use the +# workflow's Zig target. +for tool in cc c++; do + printf '#!/bin/bash\nargs=()\nfor arg in "$@"; do\n case "$arg" in\n --target=*) ;;\n *) args+=("$arg") ;;\n esac\ndone\nexec "%s" %s --target=%s "${args[@]}"\n' \ + "$ZIG" "$tool" "$RUST_BUILD_ZIG_TARGET" > "/tmp/zig-musl/${tool}" + chmod +x "/tmp/zig-musl/${tool}" +done + +target_env=${RUST_BUILD_TARGET//[-.]/_} +target_env_upper=${target_env^^} +export "CC_${target_env}=/tmp/zig-musl/cc" +export "CXX_${target_env}=/tmp/zig-musl/c++" +export "CARGO_TARGET_${target_env_upper}_LINKER=/tmp/zig-musl/cc" +export "CARGO_TARGET_${target_env_upper}_RUSTFLAGS=-Clink-self-contained=no" +# Workflow parity: CLI-specific C++ runtime selection in "Build ". +export CXXSTDLIB=c++ + +# Workflow parity: Cargo invocation from "Build ()". +args=( + build + --release + --target "$RUST_BUILD_TARGET" + -p "$RUST_BUILD_CRATE" + --bin "$RUST_BUILD_BINARY" +) +if [[ -n "$RUST_BUILD_FEATURES" ]]; then + args+=(--features "$RUST_BUILD_FEATURES") +fi + +mise x -- cargo "${args[@]}" +""" + + verify_script = r""" +set -euo pipefail +# Workflow parity: "Verify packaged binary". +binary="target/${RUST_BUILD_TARGET}/release/${RUST_BUILD_BINARY}" +output="$("$binary" --version)" +echo "$output" +grep -q "^${RUST_BUILD_BINARY} " <<<"$output" +ldd --version +ldd "$binary" || true +""" + + stage_script = r""" +set -euo pipefail +# Workflow parity: "Stage binary for prebuilt layout". +binary="target/${RUST_BUILD_TARGET}/release/${RUST_BUILD_BINARY}" +mkdir -p /out +install -m 0755 "$binary" "/out/${RUST_BUILD_BINARY}" +""" + + return ( + container.with_exec(["bash", "-ec", build_script]) + .with_exec(["bash", "-ec", verify_script]) + .with_exec(["bash", "-ec", stage_script]) + .directory("/out") + ) diff --git a/.dagger/tests/test_main.py b/.dagger/tests/test_main.py new file mode 100644 index 0000000000..1381a4b21c --- /dev/null +++ b/.dagger/tests/test_main.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest + +from open_shell.main import _rust_build_spec, _validate_inputs + + +class RustNativeBuildTest(unittest.TestCase): + def test_amd64_cli_spec(self) -> None: + spec = _rust_build_spec("cli", "amd64") + + self.assertEqual(spec.binary, "openshell") + self.assertEqual(spec.rust_target, "x86_64-unknown-linux-musl") + self.assertEqual(spec.zig_target, "x86_64-linux-musl") + + def test_arm64_cli_spec(self) -> None: + spec = _rust_build_spec("cli", "arm64") + + self.assertEqual(spec.rust_target, "aarch64-unknown-linux-musl") + self.assertEqual(spec.zig_target, "aarch64-linux-musl") + + def test_rejects_unimplemented_component(self) -> None: + with self.assertRaisesRegex(ValueError, "added incrementally"): + _rust_build_spec("gateway", "amd64") + + def test_validates_version_and_features(self) -> None: + _validate_inputs("0.12.3-rc.1", "feature-a,feature_b") + + with self.assertRaisesRegex(ValueError, "cargo-version"): + _validate_inputs("not a version", "") + with self.assertRaisesRegex(ValueError, "features"): + _validate_inputs("0.12.3", "feature; echo unsafe") + + +if __name__ == "__main__": + unittest.main() diff --git a/.dagger/uv.lock b/.dagger/uv.lock new file mode 100644 index 0000000000..4d00a751bb --- /dev/null +++ b/.dagger/uv.lock @@ -0,0 +1,643 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "anyio" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "dagger-io" +version = "0.0.0" +source = { editable = "sdk" } +dependencies = [ + { name = "anyio" }, + { name = "beartype" }, + { name = "cattrs" }, + { name = "exceptiongroup" }, + { name = "gql", extra = ["httpx"] }, + { name = "httpcore" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "yarl" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=3.6.2" }, + { name = "beartype", specifier = ">=0.22.0" }, + { name = "cattrs", specifier = ">=25.1.0" }, + { name = "exceptiongroup", specifier = ">=1.3.0" }, + { name = "gql", extras = ["httpx"], specifier = ">=4.0" }, + { name = "httpcore", specifier = ">=1.0.8" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.23.0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.54b1" }, + { name = "opentelemetry-sdk", specifier = ">=1.23.0" }, + { name = "platformdirs", specifier = ">=2.6.2" }, + { name = "rich", specifier = ">=10.11.0" }, + { name = "typing-extensions", specifier = ">=4.13.0" }, + { name = "yarl", specifier = "!=1.24.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "aiohttp", specifier = ">=3.9.3" }, + { name = "codegen", editable = "sdk/codegen" }, + { name = "mypy", specifier = ">=1.8.0" }, + { name = "pytest", specifier = ">=8.0.2" }, + { name = "pytest-httpx", specifier = ">=0.30.0" }, + { name = "pytest-mock", specifier = ">=3.12.0" }, + { name = "pytest-subprocess", specifier = ">=1.5.0" }, + { name = "ruff", specifier = ">=0.3.4" }, + { name = "sphinx", specifier = ">=7.2.6" }, + { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "gql" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "backoff" }, + { name = "graphql-core" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/9f/cf224a88ed71eb223b7aa0b9ff0aa10d7ecc9a4acdca2279eb046c26d5dc/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", size = 215644, upload-time = "2025-08-17T14:32:35.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/94/30bbd09e8d45339fa77a48f5778d74d47e9242c11b3cd1093b3d994770a5/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479", size = 89900, upload-time = "2025-08-17T14:32:34.029Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "httpx" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "open-shell" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "dagger-io" }, +] + +[package.metadata] +requires-dist = [{ name = "dagger-io", editable = "sdk" }] + +[[package]] +name = "opentelemetry-api" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197, upload-time = "2026-06-24T15:20:00.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048, upload-time = "2026-06-24T15:19:41.264Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/92/0b9f56412483a8891d4843890294796c9df8ab42417bd9bad8035d840cb3/opentelemetry_exporter_otlp_proto_http-1.43.0.tar.gz", hash = "sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f", size = 25406, upload-time = "2026-06-24T15:20:01.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/20/b685ed7af2e17c29ffc8af56f1fa8bc2033258fc30fb0d2b722f49d13ba0/opentelemetry_exporter_otlp_proto_http-1.43.0-py3-none-any.whl", hash = "sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610", size = 21795, upload-time = "2026-06-24T15:19:43.164Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/97/02fe6e1c8b1ffac42d0b429c18080edb24e0e0d18c86612edf72b5752382/opentelemetry_instrumentation-0.64b0.tar.gz", hash = "sha256:b47d528dead6271d7743114417eb67fc915bd9258111c48dbf9a4951d2efa88d", size = 41935, upload-time = "2026-06-24T15:19:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/0c/cb9fe342de5299c7af24582eb7d788661cc53a1c4b904da92309caaa9417/opentelemetry_instrumentation-0.64b0-py3-none-any.whl", hash = "sha256:133ab7ffca796557aec059bf6be3190a34b6dea987f25be3d9409e230cbdad8b", size = 35880, upload-time = "2026-06-24T15:18:17.277Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/8e/3b7191ba5817f139867612e960fbe06a69fe0d522d3b2c5e9c8e89b3ef8c/opentelemetry_instrumentation_logging-0.64b0.tar.gz", hash = "sha256:6435b4d215bf8183e15f7e3416a10ebe1c411810d3411fbfc62667daae3abacb", size = 20000, upload-time = "2026-06-24T15:19:30.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/07/9f97b0ccbb38363d12c7a2bfe44912ae7440c97aae250e2eedb32739f662/opentelemetry_instrumentation_logging-0.64b0-py3-none-any.whl", hash = "sha256:9b56f19648798ddb331c206b74d36c5f2a71e5c6b771e96298f167b2b590e40d", size = 16062, upload-time = "2026-06-24T15:18:44.019Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481, upload-time = "2026-06-24T15:20:07.625Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489, upload-time = "2026-06-24T15:19:51.164Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.64b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, + { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, + { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, + { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, + { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, + { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] diff --git a/.daggerignore b/.daggerignore new file mode 100644 index 0000000000..037b06e2a8 --- /dev/null +++ b/.daggerignore @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +/.git +/target +/.cache +/.venv +/.dagger/.venv +/.dagger/sdk +/architecture/plans +/artifacts +/deploy/docker/.build +/e2e/rust/target diff --git a/dagger.json b/dagger.json new file mode 100644 index 0000000000..7d2d589be0 --- /dev/null +++ b/dagger.json @@ -0,0 +1,8 @@ +{ + "name": "OpenShell", + "engineVersion": "v0.21.7", + "sdk": { + "source": "python" + }, + "source": ".dagger" +} diff --git a/flake.nix b/flake.nix index 13c4857bc6..6df8f621ce 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,10 @@ overlays = [ (import rust-overlay) ]; }; rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + daggerConfig = builtins.fromJSON (builtins.readFile ./dagger.json); + dagger = pkgs.callPackage ./nix/dagger.nix { + version = pkgs.lib.removePrefix "v" daggerConfig.engineVersion; + }; treefmtEval = treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; programs.nixfmt.enable = true; @@ -42,6 +46,8 @@ devShells.default = pkgs.mkShell { packages = with pkgs; [ rustToolchain + # Reproducible local build and packaging pipelines. + dagger # Required to find packages pkg-config # Required for bindgen generation. diff --git a/nix/dagger.nix b/nix/dagger.nix new file mode 100644 index 0000000000..eafd0df4c4 --- /dev/null +++ b/nix/dagger.nix @@ -0,0 +1,52 @@ +{ + fetchurl, + lib, + stdenvNoCC, + version, +}: + +let + sources = { + aarch64-darwin = { + platform = "darwin_arm64"; + hash = "sha256-uxRyzXHv5AuaXRkpROsLCV6PVWbhh50Whk8tYOednHc="; + }; + aarch64-linux = { + platform = "linux_arm64"; + hash = "sha256-eA6N3EJpru6U7yGrfLBsou4Eq+xUhuulP0mnVJ2ZY30="; + }; + x86_64-linux = { + platform = "linux_amd64"; + hash = "sha256-REMK/G+cOQ/EfE81KxXekwml6X69GuVjg5YX1t+OjMU="; + }; + }; + source = + sources.${stdenvNoCC.hostPlatform.system} + or (throw "unsupported Dagger system: ${stdenvNoCC.hostPlatform.system}"); +in +stdenvNoCC.mkDerivation { + pname = "dagger"; + inherit version; + + src = fetchurl { + url = "https://github.com/dagger/dagger/releases/download/v${version}/dagger_v${version}_${source.platform}.tar.gz"; + inherit (source) hash; + }; + + sourceRoot = "."; + dontBuild = true; + + installPhase = '' + runHook preInstall + install -Dm755 dagger "$out/bin/dagger" + runHook postInstall + ''; + + meta = { + description = "Application delivery engine for CI/CD"; + homepage = "https://dagger.io"; + license = lib.licenses.asl20; + mainProgram = "dagger"; + platforms = builtins.attrNames sources; + }; +} From 4cfc72f88341fbb6482d640917e5fa1be6a225cc Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Wed, 8 Jul 2026 15:40:28 -0700 Subject: [PATCH 2/5] feat(dev): add initial xtask migration scaffold Add a cargo xtask facade with mise fallback and migrate the Rust formatting check as the first native task. This partial implementation sets the stage for replacing mise with Nix-managed tools and native xtasks. Signed-off-by: Kris Hicks --- .cargo/config.toml | 3 + .github/workflows/branch-checks.yml | 2 +- .github/workflows/rust-cache-seed.yml | 2 +- Cargo.lock | 4 + crates/xtask/Cargo.toml | 13 +++ crates/xtask/src/main.rs | 141 ++++++++++++++++++++++++++ tasks/rust.toml | 2 +- 7 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 crates/xtask/Cargo.toml create mode 100644 crates/xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 0005fc2bd0..8ba7dbaa7e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,3 +6,6 @@ # the header lives in /usr/include/z3/ rather than /usr/include/. The extra -I # is harmless on systems where the path doesn't exist. BINDGEN_EXTRA_CLANG_ARGS = "-I/usr/include/z3" + +[alias] +xtask = "run --package xtask --" diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 7713febb6f..2adc11e8dd 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -114,7 +114,7 @@ jobs: cache-on-failure: "true" - name: Format - run: mise run rust:format:check + run: cargo xtask run rust:format:check - name: Lint run: mise run rust:lint diff --git a/.github/workflows/rust-cache-seed.yml b/.github/workflows/rust-cache-seed.yml index deb33eb835..875bf50a14 100644 --- a/.github/workflows/rust-cache-seed.yml +++ b/.github/workflows/rust-cache-seed.yml @@ -51,7 +51,7 @@ jobs: cache-on-failure: true - name: Format - run: mise run rust:format:check + run: cargo xtask run rust:format:check - name: Lint run: mise run rust:lint diff --git a/Cargo.lock b/Cargo.lock index 13b670f558..6195d40d65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7790,6 +7790,10 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "xtask" +version = "0.0.0" + [[package]] name = "xterm-color" version = "1.0.2" diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml new file mode 100644 index 0000000000..e049023504 --- /dev/null +++ b/crates/xtask/Cargo.toml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "xtask" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs new file mode 100644 index 0000000000..10e0fd6b8d --- /dev/null +++ b/crates/xtask/src/main.rs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::env; +use std::ffi::OsString; +use std::process::{Command, ExitCode, ExitStatus}; + +const USAGE: &str = "Usage: cargo xtask run [--no-deps] [--skip-deps] [-- ...]"; + +fn main() -> ExitCode { + match run(env::args_os().skip(1)) { + Ok(status) => exit_code(status), + Err(message) => { + eprintln!("error: {message}"); + eprintln!("{USAGE}"); + ExitCode::FAILURE + } + } +} + +fn run(args: impl Iterator) -> Result { + let command = RunCommand::parse(args)?; + + match command.task.to_str() { + Some("rust:format:check") => rust_format_check(&command.task_args), + _ => legacy_mise_task(&command), + } +} + +struct RunCommand { + mise_options: Vec, + task: OsString, + task_args: Vec, +} + +impl RunCommand { + fn parse(mut args: impl Iterator) -> Result { + match args.next().as_deref() { + Some(command) if command == "run" => {} + Some(command) => { + return Err(format!( + "unknown xtask command: {}", + command.to_string_lossy() + )); + } + None => return Err("missing xtask command".to_owned()), + } + + let mut mise_options = Vec::new(); + let task = loop { + match args.next() { + Some(argument) if argument == "--no-deps" || argument == "--skip-deps" => { + mise_options.push(argument); + } + Some(argument) if argument == "--" => { + return Err("missing task name before `--`".to_owned()); + } + Some(argument) => break argument, + None => return Err("missing task name".to_owned()), + } + }; + + let task_args = match args.next() { + Some(separator) if separator == "--" => args.collect(), + Some(argument) => std::iter::once(argument).chain(args).collect(), + None => Vec::new(), + }; + + Ok(Self { + mise_options, + task, + task_args, + }) + } +} + +fn rust_format_check(task_args: &[OsString]) -> Result { + let cargo = env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); + spawn( + Command::new(cargo) + .args(["fmt", "--all", "--", "--check"]) + .args(task_args), + "cargo fmt", + ) +} + +fn legacy_mise_task(command: &RunCommand) -> Result { + let mut process = Command::new("mise"); + process + .arg("run") + .args(&command.mise_options) + .arg(&command.task); + + if !command.task_args.is_empty() { + process.arg("--").args(&command.task_args); + } + + spawn(&mut process, "mise run") +} + +fn spawn(command: &mut Command, description: &str) -> Result { + command + .status() + .map_err(|error| format!("failed to execute {description}: {error}")) +} + +fn exit_code(status: ExitStatus) -> ExitCode { + match status.code() { + Some(code) => ExitCode::from(u8::try_from(code).unwrap_or(1)), + None => ExitCode::FAILURE, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_legacy_options_and_task_arguments() { + let command = RunCommand::parse( + [ + "run", + "--no-deps", + "--skip-deps", + "e2e:rust", + "--", + "--nocapture", + ] + .into_iter() + .map(OsString::from), + ) + .expect("command should parse"); + + assert_eq!( + command.mise_options, + [OsString::from("--no-deps"), OsString::from("--skip-deps")] + ); + assert_eq!(command.task, "e2e:rust"); + assert_eq!(command.task_args, [OsString::from("--nocapture")]); + } +} diff --git a/tasks/rust.toml b/tasks/rust.toml index a8856377fe..f039471209 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -23,7 +23,7 @@ hide = true ["rust:format:check"] description = "Check Rust formatting" -run = "cargo fmt --all -- --check" +run = "cargo xtask run rust:format:check" hide = true ["rust:verify:telemetry-off"] From 9d127242e963e471e8e347b99d962a048d224233 Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Thu, 9 Jul 2026 12:43:39 -0700 Subject: [PATCH 3/5] feat(dagger): add Debian release artifact packaging Add a Dagger function that assembles a Debian package using a Dagger-built CLI binary and provided gateway and VM driver binaries, mirroring the existing GitHub Actions packaging flow. Signed-off-by: Kris Hicks --- .dagger/src/open_shell/main.py | 118 ++++++++++++++++++++++++++++++--- .dagger/tests/test_main.py | 12 +++- .daggerignore | 2 - architecture/build.md | 7 ++ 4 files changed, 127 insertions(+), 12 deletions(-) diff --git a/.dagger/src/open_shell/main.py b/.dagger/src/open_shell/main.py index 240662f138..561e8e7685 100644 --- a/.dagger/src/open_shell/main.py +++ b/.dagger/src/open_shell/main.py @@ -25,8 +25,15 @@ "rust-toolchain.toml", ] +DEB_PACKAGE_SOURCE_INCLUDE = [ + "LICENSE", + "deploy/deb/", + "tasks/scripts/package-deb.sh", +] + _CARGO_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$") _FEATURES_RE = re.compile(r"^[0-9A-Za-z_, -]*$") +_DEB_VERSION_RE = re.compile(r"^[0-9A-Za-z.+~:-]+$") @dataclass(frozen=True) @@ -77,6 +84,27 @@ def _validate_inputs(cargo_version: str, features: str) -> None: raise ValueError("features contains unsupported characters") +def _validate_deb_version(deb_version: str) -> None: + if not deb_version or not _DEB_VERSION_RE.fullmatch(deb_version): + raise ValueError("deb-version contains unsupported characters") + + +def _image_container( + platform: dagger.Platform, + image: str, + github_username: str, + github_token: dagger.Secret | None, +) -> dagger.Container: + base = dag.container(platform=platform) + if github_token is not None: + if not github_username: + raise ValueError( + "github-username is required when github-token is provided" + ) + base = base.with_registry_auth("ghcr.io", github_username, github_token) + return base.from_(image) + + @object_type class OpenShell: """Build reproducible OpenShell artifacts.""" @@ -103,16 +131,13 @@ def rust_native_build( include=RUST_BUILD_SOURCE_INCLUDE, gitignore=True, ) - base = dag.container(platform=spec.platform) - if github_token is not None: - if not github_username: - raise ValueError( - "github-username is required when github-token is provided" - ) - base = base.with_registry_auth("ghcr.io", github_username, github_token) - container = ( - base.from_(toolchain_image) + _image_container( + spec.platform, + toolchain_image, + github_username, + github_token, + ) .with_directory(WORKDIR, project_source) .with_workdir(WORKDIR) # Workflow parity: "Cache Rust target and registry". @@ -242,3 +267,78 @@ def rust_native_build( .with_exec(["bash", "-ec", stage_script]) .directory("/out") ) + + @function + def deb_package( + self, + gateway_binary: dagger.File, + driver_vm_binary: dagger.File, + deb_version: str, + source_path: str = ".", + arch: str = "amd64", + cargo_version: str = "", + image_tag: str = "dagger", + toolchain_image: str = DEFAULT_TOOLCHAIN_IMAGE, + github_username: str = "", + github_token: dagger.Secret | None = None, + ) -> dagger.Directory: + """Build a Debian package using a Dagger-built CLI binary.""" + spec = _rust_build_spec("cli", arch) + _validate_deb_version(deb_version) + + cli_binary = self.rust_native_build( + source_path=source_path, + component="cli", + arch=arch, + cargo_version=cargo_version, + image_tag=image_tag, + features="bundled-z3", + toolchain_image=toolchain_image, + github_username=github_username, + github_token=github_token, + ).file("openshell") + package_source = dag.current_workspace().directory( + source_path, + include=DEB_PACKAGE_SOURCE_INCLUDE, + gitignore=True, + ) + + # Workflow parity: "Download artifact" and + # "Extract package inputs" in deb-package.yml. Typed File inputs avoid + # the Actions artifact/tar transport while preserving the same paths. + container = ( + _image_container( + spec.platform, + toolchain_image, + github_username, + github_token, + ) + .with_directory(WORKDIR, package_source) + .with_workdir(WORKDIR) + .with_file("/package-binaries/openshell", cli_binary, permissions=0o755) + .with_file( + "/package-binaries/openshell-gateway", + gateway_binary, + permissions=0o755, + ) + .with_file( + "/package-binaries/openshell-driver-vm", + driver_vm_binary, + permissions=0o755, + ) + .with_env_variable("OPENSHELL_CLI_BINARY", "/package-binaries/openshell") + .with_env_variable( + "OPENSHELL_GATEWAY_BINARY", + "/package-binaries/openshell-gateway", + ) + .with_env_variable( + "OPENSHELL_DRIVER_VM_BINARY", + "/package-binaries/openshell-driver-vm", + ) + .with_env_variable("OPENSHELL_DEB_VERSION", deb_version) + .with_env_variable("OPENSHELL_DEB_ARCH", arch) + .with_env_variable("OPENSHELL_OUTPUT_DIR", "/out") + # Workflow parity: "Build Debian package". + .with_exec(["bash", "tasks/scripts/package-deb.sh"]) + ) + return container.directory("/out") diff --git a/.dagger/tests/test_main.py b/.dagger/tests/test_main.py index 1381a4b21c..fded6db39b 100644 --- a/.dagger/tests/test_main.py +++ b/.dagger/tests/test_main.py @@ -3,7 +3,11 @@ import unittest -from open_shell.main import _rust_build_spec, _validate_inputs +from open_shell.main import ( + _rust_build_spec, + _validate_deb_version, + _validate_inputs, +) class RustNativeBuildTest(unittest.TestCase): @@ -32,6 +36,12 @@ def test_validates_version_and_features(self) -> None: with self.assertRaisesRegex(ValueError, "features"): _validate_inputs("0.12.3", "feature; echo unsafe") + def test_validates_debian_version(self) -> None: + _validate_deb_version("1:0.12.3~rc.1-2") + + with self.assertRaisesRegex(ValueError, "deb-version"): + _validate_deb_version("0.12.3; echo unsafe") + if __name__ == "__main__": unittest.main() diff --git a/.daggerignore b/.daggerignore index 037b06e2a8..6d6c54e04e 100644 --- a/.daggerignore +++ b/.daggerignore @@ -8,6 +8,4 @@ /.dagger/.venv /.dagger/sdk /architecture/plans -/artifacts -/deploy/docker/.build /e2e/rust/target diff --git a/architecture/build.md b/architecture/build.md index a3cb2e25fb..fafc54ae4d 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -79,6 +79,13 @@ step via the `rust-native-build.yml` workflow (per-architecture, per-component) and uploads the result as an artifact that the image build job downloads back into the staging directory before running Buildx. +The Dagger module provides a local, architecture-selectable build path for the +Linux CLI and can assemble a Debian package with that CLI plus prebuilt gateway +and VM driver binaries. It uses the same CI toolchain image and +`tasks/scripts/package-deb.sh` as the GitHub Actions workflows. The gateway and +VM driver remain explicit file inputs until their native builds move into +Dagger. + Runtime layout: - **Gateway**: `gcr.io/distroless/cc-debian13:nonroot` base, GNU-linked binary at From a368f350768fdfc321c8f3cacd630d5a5c6f1b8a Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Thu, 9 Jul 2026 12:43:57 -0700 Subject: [PATCH 4/5] feat(xtask): add release smoke test VM harness Add a cargo xtask release-smoke-test command that accepts a provided .deb package, provisions a Lima VM with rootless Podman, installs the artifact, starts the packaged gateway, creates a sandbox, and verifies that curl is denied without policy. Signed-off-by: Kris Hicks --- architecture/build.md | 25 ++ .../xtask/scripts/install-podman-rootless.sh | 44 ++ .../release-smoke/ubuntu-podman-rootless.sh | 117 ++++++ crates/xtask/src/lima.rs | 383 ++++++++++++++++++ crates/xtask/src/main.rs | 20 +- crates/xtask/src/release_smoke_test.rs | 290 +++++++++++++ crates/xtask/src/vm.rs | 65 +++ flake.nix | 4 + 8 files changed, 945 insertions(+), 3 deletions(-) create mode 100644 crates/xtask/scripts/install-podman-rootless.sh create mode 100644 crates/xtask/scripts/release-smoke/ubuntu-podman-rootless.sh create mode 100644 crates/xtask/src/lima.rs create mode 100644 crates/xtask/src/release_smoke_test.rs create mode 100644 crates/xtask/src/vm.rs diff --git a/architecture/build.md b/architecture/build.md index fafc54ae4d..041aca108d 100644 --- a/architecture/build.md +++ b/architecture/build.md @@ -86,6 +86,31 @@ and VM driver binaries. It uses the same CI toolchain image and VM driver remain explicit file inputs until their native builds move into Dagger. +`cargo xtask release-smoke-test --deb ` validates a Debian release +artifact in an architecture-specific `os-ubuntu---podman-rl` +Lima VM. +Ubuntu 26.04 is the default; `--guest-os` selects another supported versioned +Lima template. Lima starts in plain mode so the test provisions the rootless +Podman compute-driver environment itself without Lima-managed containerd, +mounts, or port forwarding. Snapshot reuse is +opt-in with `--snapshot`: when QEMU is available, the xtask selects it +explicitly, stores a `base-v1` snapshot, and restores that snapshot before each +artifact test. When QEMU is unavailable or snapshotting is not requested, Lima +uses its default backend and the xtask deletes the VM after the test. It starts +the packaged gateway, creates a sandbox, and verifies that the default policy +denies an undeclared network request. Guest preparation is split by OS/driver +combination: `scripts/install-podman-rootless.sh` provisions the rootless Podman +compute-driver environment, and each supported release smoke path has its own +script under `scripts/release-smoke/`. The current supported release path is +`ubuntu-podman-rootless.sh`; adding Fedora rootless Podman support should add a +Fedora-specific script there and a matching `release_smoke_guest_script` arm in +`crates/xtask/src/release_smoke_test.rs`. If multiple scripts later share a +proven chunk of logic, extract that chunk then. The Ubuntu path temporarily +writes the Podman-ready gateway bind configuration that Debian packages do not +yet seed; remove that workaround when Debian first-start configuration matches +RPM. CI jobs omit `--snapshot` while still benefiting from Lima's cached +guest-image download. + Runtime layout: - **Gateway**: `gcr.io/distroless/cc-debian13:nonroot` base, GNU-linked binary at diff --git a/crates/xtask/scripts/install-podman-rootless.sh b/crates/xtask/scripts/install-podman-rootless.sh new file mode 100644 index 0000000000..6a0de6815e --- /dev/null +++ b/crates/xtask/scripts/install-podman-rootless.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -Eeuo pipefail + +install_ubuntu() { + echo "==> Installing rootless Podman on Ubuntu" + sudo apt-get update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y \ + ca-certificates \ + curl \ + podman +} + +enable_rootless_podman_socket() { + echo "==> Enabling the rootless Podman socket" + sudo loginctl enable-linger "$USER" + systemctl --user daemon-reload + systemctl --user enable --now podman.socket + systemctl --user is-active --quiet podman.socket + test "$(podman info --format '{{.Host.Security.Rootless}}')" = true +} + +if [ ! -r /etc/os-release ]; then + echo "cannot detect guest OS: /etc/os-release is missing" >&2 + exit 1 +fi + +# shellcheck disable=SC1091 +. /etc/os-release + +# Add OS-specific package installation branches here as guest support expands. +case "${ID:-}" in +ubuntu) + install_ubuntu + ;; +*) + echo "unsupported guest OS for rootless Podman setup: ${ID:-unknown}" >&2 + exit 1 + ;; +esac + +enable_rootless_podman_socket diff --git a/crates/xtask/scripts/release-smoke/ubuntu-podman-rootless.sh b/crates/xtask/scripts/release-smoke/ubuntu-podman-rootless.sh new file mode 100644 index 0000000000..b1905fea2c --- /dev/null +++ b/crates/xtask/scripts/release-smoke/ubuntu-podman-rootless.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -Eeuo pipefail + +# Require xtask to provide the artifact path that it copied into the VM. +if [ -z "${OPENSHELL_RELEASE_ARTIFACT:-}" ]; then + echo "OPENSHELL_RELEASE_ARTIFACT must be set" >&2 + exit 1 +fi +artifact_path=$OPENSHELL_RELEASE_ARTIFACT +sandbox_name=${OPENSHELL_RELEASE_SMOKE_SANDBOX:-release-smoke-curl} + +# Confirm that this OS+driver script is running on the guest OS it supports. +if [ ! -r /etc/os-release ]; then + echo "cannot detect guest OS: /etc/os-release is missing" >&2 + exit 1 +fi +# shellcheck disable=SC1091 +. /etc/os-release +if [ "${ID:-}" != "ubuntu" ]; then + echo "ubuntu-podman-rootless release smoke test requires Ubuntu, got ${ID:-unknown}" >&2 + exit 1 +fi + +# On failure, collect enough state to diagnose gateway startup or sandbox launch. +# shellcheck disable=SC2154 +trap ' +status=$? +trap - EXIT +if [ "$status" -ne 0 ]; then + echo "==> OpenShell release smoke test diagnostics" >&2 + openshell status >&2 || true + systemctl --user status openshell-gateway --no-pager >&2 || true + systemctl --user status podman.socket --no-pager >&2 || true + journalctl --user -u openshell-gateway --no-pager -n 200 >&2 || true + ss -ltnp >&2 || true + if command -v podman >/dev/null 2>&1; then + podman info >&2 || true + podman ps -a >&2 || true + while IFS= read -r container_id; do + [ -n "$container_id" ] || continue + echo "==> Podman logs: $container_id" >&2 + podman logs "$container_id" >&2 || true + done < <(podman ps -aq) + fi +fi +openshell sandbox delete "$sandbox_name" >/dev/null 2>&1 || true +exit "$status" +' EXIT + +# Install the Ubuntu Debian release artifact under test. +echo "==> Installing Ubuntu release artifact $artifact_path" +sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y "$artifact_path" +openshell --version +openshell-gateway --version + +# Verify that VM setup left the rootless Podman compute driver available. +echo "==> Verifying the rootless Podman compute driver" +systemctl --user is-active --quiet podman.socket +test "$(podman info --format '{{.Host.Security.Rootless}}')" = true + +# Temporary release-test workaround: Debian packages do not yet seed the +# Podman-ready gateway configuration that RPM packages install. Remove this when +# Debian first-start configuration owns the equivalent behavior. +echo "==> Applying the temporary Debian Podman gateway workaround" +mkdir -p "$HOME/.config/openshell" +cat >"$HOME/.config/openshell/gateway.toml" <<'EOF' +[openshell] +version = 1 + +[openshell.gateway] +bind_address = "0.0.0.0:17670" +compute_drivers = ["podman"] +EOF + +# Start the packaged gateway service and register it with the CLI. +echo "==> Starting the packaged OpenShell gateway" +systemctl --user enable --now openshell-gateway +systemctl --user is-active --quiet openshell-gateway + +echo "==> Registering the packaged gateway" +openshell gateway add https://127.0.0.1:17670 --local --name openshell + +# Wait until the CLI can reach the packaged gateway. +echo "==> Waiting for the packaged gateway" +status_output="" +for _ in $(seq 1 30); do + if status_output="$(NO_COLOR=1 openshell status 2>&1)" && grep -q "Version:" <<<"$status_output"; then + break + fi + sleep 1 +done +if ! grep -q "Version:" <<<"$status_output"; then + echo "openshell status did not report a connected gateway:" >&2 + echo "$status_output" >&2 + exit 1 +fi + +# Create a sandbox and verify that the default policy denies undeclared egress. +echo "==> Creating a sandbox and verifying default-deny networking" +timeout 600 openshell sandbox create \ + --name "$sandbox_name" \ + --no-tty \ + -- \ + sh -c ' + command -v curl >/dev/null + if curl --fail --show-error --silent --max-time 20 https://example.com; then + echo "curl unexpectedly succeeded without a network policy" >&2 + exit 1 + fi + echo "curl was denied as expected" + ' + +# Report the single result this smoke test is meant to prove. +echo "==> Ubuntu rootless Podman release smoke test passed: sandbox curl was denied as expected" diff --git a/crates/xtask/src/lima.rs b/crates/xtask/src/lima.rs new file mode 100644 index 0000000000..5764171b58 --- /dev/null +++ b/crates/xtask/src/lima.rs @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Write; +use std::path::Path; +use std::process::{Command, ExitStatus, Stdio}; + +use crate::vm::{Arch, ComputeDriver, ProvisionOptions, VmInstance, VmProvider, VmSpec}; + +const BASE_SNAPSHOT: &str = "base-v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Backend { + Default, + Qemu, +} + +impl Backend { + const fn name(self) -> &'static str { + match self { + Self::Default => "def", + Self::Qemu => "qemu", + } + } + + const fn lima(self) -> Option<&'static str> { + match self { + Self::Default => None, + Self::Qemu => Some("qemu"), + } + } +} + +fn instance_name(spec: VmSpec, backend: Backend, ephemeral: bool) -> String { + let arch = match spec.arch { + Arch::Amd64 => "amd64", + Arch::Arm64 => "arm64", + }; + let compute_driver = match spec.compute_driver { + ComputeDriver::PodmanRootless => "podman-rl", + }; + let guest = format!( + "{}{}", + spec.guest_os.distribution, + spec.guest_os.version.replace('.', "") + ); + let base = format!("os-{guest}-{arch}-{}-{compute_driver}", backend.name()); + if ephemeral { + format!("{base}-{}", std::process::id()) + } else { + base + } +} + +pub struct LimaProvider; + +pub struct LimaVm { + name: String, + reusable: bool, +} + +impl VmProvider for LimaProvider { + type Instance = LimaVm; + + fn provision( + &self, + spec: VmSpec, + options: ProvisionOptions, + setup_script: &str, + ) -> Result { + checked( + Command::new("limactl").arg("--version"), + "check the Lima installation", + )?; + + let qemu_available = options.snapshot && driver_available("qemu")?; + let reusable = options.snapshot && qemu_available; + if options.snapshot && !qemu_available { + eprintln!( + "warning: QEMU is not available; using Lima's default backend without snapshots" + ); + } + + let backend = if reusable { + Backend::Qemu + } else { + Backend::Default + }; + let name = instance_name(spec, backend, !reusable); + + prepare_instance( + &name, + spec, + backend, + options.rebuild, + reusable, + setup_script, + )?; + Ok(LimaVm { name, reusable }) + } +} + +impl VmInstance for LimaVm { + fn name(&self) -> &str { + &self.name + } + + fn copy_file(&self, source: &Path, destination: &str) -> Result<(), String> { + let guest_path = format!("{}:{destination}", self.name); + checked( + Command::new("limactl") + .args(["--tty=false", "copy", "--backend=scp"]) + .arg(source) + .arg(guest_path), + "copy a file into Lima", + ) + } + + fn run_script(&self, script: &str, description: &str) -> Result { + run_guest_script(&self.name, script, description) + } + + fn cleanup(&self) -> Result<(), String> { + if self.reusable { + stop_instance(&self.name) + } else { + delete_instance(&self.name, "delete the Lima test instance") + } + } +} + +fn prepare_instance( + instance: &str, + spec: VmSpec, + backend: Backend, + rebuild: bool, + use_snapshot: bool, + setup_script: &str, +) -> Result<(), String> { + let status = instance_status(instance)?; + let can_restore = use_snapshot + && !rebuild + && matches!(status.as_deref(), Some("Running" | "Stopped")) + && snapshot_exists(instance, BASE_SNAPSHOT)?; + + if can_restore { + stop_instance(instance)?; + println!("==> Restoring Lima snapshot {BASE_SNAPSHOT}"); + checked( + Command::new("limactl").args([ + "--tty=false", + "snapshot", + "apply", + instance, + "--tag", + BASE_SNAPSHOT, + ]), + "restore the Lima test snapshot", + )?; + } else { + if status.is_some() { + println!("==> Rebuilding Lima instance {instance}"); + delete_instance(instance, "delete the stale Lima test instance")?; + } + + start_new_instance(instance, spec, backend)?; + let setup_status = run_guest_script(instance, setup_script, "VM setup")?; + if !setup_status.success() { + return Err(format!( + "VM setup failed with exit code {}", + display_exit_code(setup_status) + )); + } + + if use_snapshot { + stop_instance(instance)?; + println!("==> Creating Lima snapshot {BASE_SNAPSHOT}"); + checked( + Command::new("limactl").args([ + "--tty=false", + "snapshot", + "create", + instance, + "--tag", + BASE_SNAPSHOT, + ]), + "create the Lima test snapshot", + )?; + } else { + return Ok(()); + } + } + + checked( + Command::new("limactl").args(["--tty=false", "start", instance]), + "start the prepared Lima test instance", + ) +} + +fn start_new_instance(instance: &str, spec: VmSpec, backend: Backend) -> Result<(), String> { + println!("==> Creating Lima instance {instance}"); + let arch = match spec.arch { + Arch::Amd64 => "x86_64", + Arch::Arm64 => "aarch64", + }; + let lima_template = format!( + "template:{}-{}", + spec.guest_os.distribution, spec.guest_os.version + ); + let mut process = Command::new("limactl"); + process.args([ + "--tty=false", + "start", + "--plain", + "--name", + instance, + "--arch", + arch, + "--cpus", + "4", + "--memory", + "8", + "--disk", + "30", + ]); + if let Some(vm_type) = backend.lima() { + process.args(["--vm-type", vm_type]); + } + process.arg(lima_template); + checked(&mut process, "create the Lima test instance") +} + +fn driver_available(driver: &str) -> Result { + let output = Command::new("limactl") + .args(["start", "--list-drivers"]) + .output() + .map_err(|error| format!("failed to list Lima VM drivers: {error}"))?; + if !output.status.success() { + return Err("failed to list Lima VM drivers".to_owned()); + } + + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .any(|available| available.trim() == driver)) +} + +fn instance_status(instance: &str) -> Result, String> { + let output = Command::new("limactl") + .args(["list", "--format", "{{.Name}}\t{{.Status}}"]) + .output() + .map_err(|error| format!("failed to inspect Lima instance {instance}: {error}"))?; + if !output.status.success() { + return Err(format!("failed to inspect Lima instance {instance}")); + } + + Ok(parse_instance_status(&output.stdout, instance)) +} + +fn parse_instance_status(output: &[u8], instance: &str) -> Option { + String::from_utf8_lossy(output).lines().find_map(|line| { + let (name, status) = line.split_once('\t')?; + (name == instance).then(|| status.to_owned()) + }) +} + +fn snapshot_exists(instance: &str, tag: &str) -> Result { + let output = Command::new("limactl") + .args(["snapshot", "list", instance, "--quiet"]) + .output() + .map_err(|error| format!("failed to list Lima snapshots for {instance}: {error}"))?; + if !output.status.success() { + return Err(format!("failed to list Lima snapshots for {instance}")); + } + + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .any(|snapshot| snapshot.trim() == tag)) +} + +fn stop_instance(instance: &str) -> Result<(), String> { + if instance_status(instance)?.as_deref() != Some("Running") { + return Ok(()); + } + + checked( + Command::new("limactl").args(["--tty=false", "stop", instance]), + "stop the Lima test instance", + ) +} + +fn delete_instance(instance: &str, description: &str) -> Result<(), String> { + checked( + Command::new("limactl").args(["--tty=false", "delete", "--force", instance]), + description, + ) +} + +fn run_guest_script(instance: &str, script: &str, description: &str) -> Result { + let mut child = Command::new("limactl") + .args(["--tty=false", "shell", instance, "bash", "-s"]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|error| format!("failed to start the Lima guest {description}: {error}"))?; + + child + .stdin + .take() + .ok_or_else(|| "failed to open stdin for the Lima guest command".to_owned())? + .write_all(script.as_bytes()) + .map_err(|error| format!("failed to send the {description} to Lima: {error}"))?; + + child + .wait() + .map_err(|error| format!("failed to wait for the Lima guest {description}: {error}")) +} + +fn checked(command: &mut Command, description: &str) -> Result<(), String> { + let status = command + .status() + .map_err(|error| format!("failed to execute {description}: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!( + "{description} failed with exit code {}", + display_exit_code(status) + )) + } +} + +fn display_exit_code(status: ExitStatus) -> String { + status + .code() + .map_or_else(|| "signal".to_owned(), |code| code.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::vm::GuestOs; + + #[test] + fn names_the_vm_for_its_environment() { + let spec = VmSpec { + guest_os: GuestOs::new("ubuntu", "24.04"), + arch: Arch::Arm64, + compute_driver: ComputeDriver::PodmanRootless, + }; + + assert_eq!( + instance_name(spec, Backend::Qemu, false), + "os-ubuntu2404-arm64-qemu-podman-rl" + ); + assert!( + instance_name(spec, Backend::Qemu, true) + .starts_with("os-ubuntu2404-arm64-qemu-podman-rl-") + ); + } + + #[test] + fn leaves_room_for_lima_socket_paths() { + let spec = VmSpec { + guest_os: GuestOs::new("ubuntu", "26.04"), + arch: Arch::Arm64, + compute_driver: ComputeDriver::PodmanRootless, + }; + + // Lima appends a PID and temporary socket suffix beneath ~/.lima. Keep + // the stable portion bounded so common macOS home paths remain below + // UNIX_PATH_MAX even with a ten-digit PID. + assert!(instance_name(spec, Backend::Default, false).len() <= 40); + } + + #[test] + fn parses_matching_instance_status() { + let output = b"other\tStopped\nos-ubuntu2404-arm64-qemu-podman-rl\tRunning\n"; + assert_eq!( + parse_instance_status(output, "os-ubuntu2404-arm64-qemu-podman-rl"), + Some("Running".to_owned()) + ); + assert_eq!(parse_instance_status(output, "missing"), None); + } +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 10e0fd6b8d..a419b09d80 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -1,11 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +mod lima; +mod release_smoke_test; +mod vm; + use std::env; use std::ffi::OsString; use std::process::{Command, ExitCode, ExitStatus}; -const USAGE: &str = "Usage: cargo xtask run [--no-deps] [--skip-deps] [-- ...]"; +const USAGE: &str = "Usage:\n cargo xtask run [--no-deps] [--skip-deps] [-- ...]\n cargo xtask release-smoke-test --deb [--arch ] [--guest-os ] [--snapshot] [--rebuild-vm] [--keep-vm]"; fn main() -> ExitCode { match run(env::args_os().skip(1)) { @@ -19,11 +23,21 @@ fn main() -> ExitCode { } fn run(args: impl Iterator) -> Result { - let command = RunCommand::parse(args)?; + let arguments = args.collect::>(); + + match arguments.first().and_then(|argument| argument.to_str()) { + Some("release-smoke-test") => release_smoke_test::run(arguments.into_iter().skip(1)), + _ => { + let command = RunCommand::parse(arguments.into_iter())?; + run_task(&command) + } + } +} +fn run_task(command: &RunCommand) -> Result { match command.task.to_str() { Some("rust:format:check") => rust_format_check(&command.task_args), - _ => legacy_mise_task(&command), + _ => legacy_mise_task(command), } } diff --git a/crates/xtask/src/release_smoke_test.rs b/crates/xtask/src/release_smoke_test.rs new file mode 100644 index 0000000000..fcbd0db3f2 --- /dev/null +++ b/crates/xtask/src/release_smoke_test.rs @@ -0,0 +1,290 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::env; +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; +use std::process::ExitStatus; + +use crate::lima::LimaProvider; +use crate::vm::{Arch, ComputeDriver, GuestOs, ProvisionOptions, VmInstance, VmProvider, VmSpec}; + +const INSTALL_PODMAN_ROOTLESS_SCRIPT: &str = include_str!("../scripts/install-podman-rootless.sh"); +const RELEASE_SMOKE_UBUNTU_PODMAN_ROOTLESS_SCRIPT: &str = + include_str!("../scripts/release-smoke/ubuntu-podman-rootless.sh"); +const GUEST_RELEASE_ARTIFACT_PATH: &str = "/tmp/openshell-release.deb"; + +pub fn run(args: impl Iterator) -> Result { + let command = ReleaseSmokeTestCommand::parse(args)?; + release_smoke_test(&LimaProvider, &command) +} + +struct ReleaseSmokeTestCommand { + deb: PathBuf, + arch: Option, + keep_vm: bool, + rebuild_vm: bool, + snapshot: bool, + guest_os: GuestOs, +} + +impl ReleaseSmokeTestCommand { + fn parse(mut args: impl Iterator) -> Result { + let mut deb = None; + let mut arch = None; + let mut keep_vm = false; + let mut rebuild_vm = false; + let mut snapshot = false; + let mut guest_os = None; + + while let Some(argument) = args.next() { + match argument.to_str() { + Some("--deb") => { + let value = args + .next() + .ok_or_else(|| "--deb requires a path".to_owned())?; + if deb.replace(PathBuf::from(value)).is_some() { + return Err("--deb may only be specified once".to_owned()); + } + } + Some("--arch") => { + let value = args + .next() + .ok_or_else(|| "--arch requires a value".to_owned())?; + if arch.replace(parse_arch(&value)?).is_some() { + return Err("--arch may only be specified once".to_owned()); + } + } + Some("--guest-os") => { + let value = args + .next() + .ok_or_else(|| "--guest-os requires a value".to_owned())?; + if guest_os.replace(parse_guest_os(&value)?).is_some() { + return Err("--guest-os may only be specified once".to_owned()); + } + } + Some("--keep-vm") => keep_vm = true, + Some("--rebuild-vm") => rebuild_vm = true, + Some("--snapshot") => snapshot = true, + Some(value) => return Err(format!("unknown release-smoke-test option: {value}")), + None => return Err("release-smoke-test options must be valid UTF-8".to_owned()), + } + } + + if rebuild_vm && !snapshot { + return Err("--rebuild-vm requires --snapshot".to_owned()); + } + + Ok(Self { + deb: deb.ok_or_else(|| "release-smoke-test requires --deb ".to_owned())?, + arch, + keep_vm, + rebuild_vm, + snapshot, + guest_os: guest_os.unwrap_or(GuestOs::new("ubuntu", "26.04")), + }) + } +} + +fn parse_arch(value: &OsStr) -> Result { + match value.to_str() { + Some("amd64" | "x86_64") => Ok(Arch::Amd64), + Some("arm64" | "aarch64") => Ok(Arch::Arm64), + Some(value) => Err(format!("unsupported architecture: {value}")), + None => Err("--arch must be valid UTF-8".to_owned()), + } +} + +fn parse_guest_os(value: &OsStr) -> Result { + match value.to_str() { + Some("ubuntu-24.04") => Ok(GuestOs::new("ubuntu", "24.04")), + Some("ubuntu-26.04") => Ok(GuestOs::new("ubuntu", "26.04")), + Some(value) => Err(format!( + "unsupported guest OS: {value} (expected ubuntu-24.04 or ubuntu-26.04)" + )), + None => Err("--guest-os must be valid UTF-8".to_owned()), + } +} + +fn release_smoke_test( + provider: &P, + command: &ReleaseSmokeTestCommand, +) -> Result { + let deb = command.deb.canonicalize().map_err(|error| { + format!( + "cannot read Debian artifact {}: {error}", + command.deb.display() + ) + })?; + if !deb.is_file() { + return Err(format!("Debian artifact is not a file: {}", deb.display())); + } + + let arch = command.arch.unwrap_or_else(|| infer_deb_arch(&deb)); + let spec = VmSpec { + guest_os: command.guest_os, + arch, + compute_driver: ComputeDriver::PodmanRootless, + }; + let setup_script = compute_driver_setup_script(spec.compute_driver); + let vm = provider.provision( + spec, + ProvisionOptions { + snapshot: command.snapshot, + rebuild: command.rebuild_vm, + }, + setup_script, + )?; + + println!("==> Testing {} with {}", deb.display(), vm.name()); + let test_script = release_smoke_guest_script(spec)?; + + let test_result = (|| { + vm.copy_file(&deb, GUEST_RELEASE_ARTIFACT_PATH)?; + vm.run_script(&test_script, "release smoke test") + })(); + + if command.keep_vm { + eprintln!("VM kept for inspection: {}", vm.name()); + return test_result; + } + + let cleanup_result = vm.cleanup(); + match (test_result, cleanup_result) { + (Ok(status), Ok(())) => Ok(status), + (Err(error), _) => Err(error), + (Ok(status), Err(error)) if status.success() => Err(error), + (Ok(status), Err(error)) => { + eprintln!("warning: {error}"); + Ok(status) + } + } +} + +fn compute_driver_setup_script(compute_driver: ComputeDriver) -> &'static str { + match compute_driver { + ComputeDriver::PodmanRootless => INSTALL_PODMAN_ROOTLESS_SCRIPT, + } +} + +fn release_smoke_guest_script(spec: VmSpec) -> Result { + match (spec.guest_os.distribution, spec.compute_driver) { + ("ubuntu", ComputeDriver::PodmanRootless) => Ok(format!( + "export OPENSHELL_RELEASE_ARTIFACT={GUEST_RELEASE_ARTIFACT_PATH}\n\ + {RELEASE_SMOKE_UBUNTU_PODMAN_ROOTLESS_SCRIPT}" + )), + (distribution, ComputeDriver::PodmanRootless) => Err(format!( + "unsupported release smoke test combination: {distribution}-{} with rootless Podman", + spec.guest_os.version + )), + } +} + +fn infer_deb_arch(path: &Path) -> Arch { + let filename = path.file_name().and_then(OsStr::to_str).unwrap_or_default(); + if filename.ends_with("_arm64.deb") || filename.ends_with("-arm64.deb") { + return Arch::Arm64; + } + if filename.ends_with("_amd64.deb") || filename.ends_with("-amd64.deb") { + return Arch::Amd64; + } + + match env::consts::ARCH { + "aarch64" => Arch::Arm64, + _ => Arch::Amd64, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_release_smoke_test_options() { + let command = ReleaseSmokeTestCommand::parse( + [ + "--deb", + "artifacts/openshell_1.2.3_arm64.deb", + "--arch", + "arm64", + "--keep-vm", + "--rebuild-vm", + "--snapshot", + "--guest-os", + "ubuntu-26.04", + ] + .into_iter() + .map(OsString::from), + ) + .expect("command should parse"); + + assert_eq!( + command.deb, + PathBuf::from("artifacts/openshell_1.2.3_arm64.deb") + ); + assert_eq!(command.arch, Some(Arch::Arm64)); + assert!(command.keep_vm); + assert!(command.rebuild_vm); + assert!(command.snapshot); + assert_eq!(command.guest_os, GuestOs::new("ubuntu", "26.04")); + } + + #[test] + fn release_smoke_test_requires_deb() { + let error = ReleaseSmokeTestCommand::parse(std::iter::empty()) + .err() + .expect("missing --deb should fail"); + + assert!(error.contains("requires --deb")); + } + + #[test] + fn release_smoke_test_defaults_to_latest_ubuntu() { + let command = ReleaseSmokeTestCommand::parse( + ["--deb", "artifacts/openshell_1.2.3_arm64.deb"] + .into_iter() + .map(OsString::from), + ) + .expect("command should parse"); + + assert_eq!(command.guest_os, GuestOs::new("ubuntu", "26.04")); + } + + #[test] + fn infers_debian_architecture_from_artifact_name() { + assert_eq!( + infer_deb_arch(Path::new("openshell_1.2.3_arm64.deb")), + Arch::Arm64 + ); + assert_eq!( + infer_deb_arch(Path::new("openshell_1.2.3_amd64.deb")), + Arch::Amd64 + ); + } + + #[test] + fn selects_the_release_smoke_script_by_os_and_driver() { + let script = release_smoke_guest_script(VmSpec { + guest_os: GuestOs::new("ubuntu", "26.04"), + arch: Arch::Arm64, + compute_driver: ComputeDriver::PodmanRootless, + }) + .expect("ubuntu podman rootless smoke test should be supported"); + + assert!(script.contains("Creating a sandbox and verifying default-deny networking")); + assert!(script.contains("Installing Ubuntu release artifact")); + assert!(script.contains("export OPENSHELL_RELEASE_ARTIFACT=/tmp/openshell-release.deb")); + } + + #[test] + fn rejects_unsupported_release_smoke_combinations() { + let error = release_smoke_guest_script(VmSpec { + guest_os: GuestOs::new("fedora", "42"), + arch: Arch::Arm64, + compute_driver: ComputeDriver::PodmanRootless, + }) + .expect_err("fedora release smoke test should not be supported yet"); + + assert!(error.contains("fedora-42 with rootless Podman")); + } +} diff --git a/crates/xtask/src/vm.rs b/crates/xtask/src/vm.rs new file mode 100644 index 0000000000..45a8bfc005 --- /dev/null +++ b/crates/xtask/src/vm.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::path::Path; +use std::process::ExitStatus; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Arch { + Amd64, + Arm64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct GuestOs { + pub distribution: &'static str, + pub version: &'static str, +} + +impl GuestOs { + pub const fn new(distribution: &'static str, version: &'static str) -> Self { + Self { + distribution, + version, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ComputeDriver { + PodmanRootless, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VmSpec { + pub guest_os: GuestOs, + pub arch: Arch, + pub compute_driver: ComputeDriver, +} + +#[derive(Debug, Clone, Copy)] +pub struct ProvisionOptions { + pub snapshot: bool, + pub rebuild: bool, +} + +pub trait VmProvider { + type Instance: VmInstance; + + fn provision( + &self, + spec: VmSpec, + options: ProvisionOptions, + setup_script: &str, + ) -> Result; +} + +pub trait VmInstance { + fn name(&self) -> &str; + + fn copy_file(&self, source: &Path, destination: &str) -> Result<(), String>; + + fn run_script(&self, script: &str, description: &str) -> Result; + + fn cleanup(&self) -> Result<(), String>; +} diff --git a/flake.nix b/flake.nix index 6df8f621ce..93a534a37e 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,10 @@ rustToolchain # Reproducible local build and packaging pipelines. dagger + # Required for running packaging tests. + (lima.override { + withAdditionalGuestAgents = true; + }) # Required to find packages pkg-config # Required for bindgen generation. From 75d02f8782aa72da72a214a88504dbe787da0079 Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Thu, 9 Jul 2026 13:02:52 -0700 Subject: [PATCH 5/5] ci(release): smoke test Debian package in Lima VM Add an Ubuntu rootless Podman release canary job that downloads the deb-linux-amd64 artifact from the triggering Release Dev run and runs cargo xtask release-smoke-test against it in a Lima VM. Signed-off-by: Kris Hicks --- .agents/skills/test-release-canary/SKILL.md | 10 ++- .github/workflows/release-canary.yml | 79 +++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/.agents/skills/test-release-canary/SKILL.md b/.agents/skills/test-release-canary/SKILL.md index 4bf7d38ae3..a7e6f1e9dd 100644 --- a/.agents/skills/test-release-canary/SKILL.md +++ b/.agents/skills/test-release-canary/SKILL.md @@ -14,9 +14,11 @@ The Release Canary (`.github/workflows/release-canary.yml`) smoke-tests the arti | `macos` | `macos-latest-xlarge` | `install.sh` resolves the Homebrew formula, brew installs the cask, and `openshell status` reaches the brew-services–backed local gateway with the VM driver. | | `ubuntu` | `ubuntu-latest` | `install.sh` installs the Debian package, the post-install systemd user service starts, and `openshell status` reaches the local gateway with the Docker driver. | | `fedora` | `fedora:latest` container | `install.sh` installs the RPM packages, the local gateway starts under Podman, and `openshell status` succeeds. | +| `ubuntu-snap` | `ubuntu-latest` | Downloads the `snap-linux-amd64` artifact from the triggering Release Dev run, installs it with `snap install --dangerous`, connects required interfaces, registers the snap gateway, and runs `openshell status`. | +| `ubuntu-podman-rootless` | `ubuntu-latest` + Lima/QEMU | Downloads the `deb-linux-amd64` artifact from the triggering Release Dev run, installs repo tools with mise, boots an Ubuntu Lima VM with rootless Podman, installs the `.deb`, starts the packaged gateway, creates a sandbox, and verifies that `curl` is denied without policy. | | `kubernetes` | `ubuntu-latest` + kind | `helm install oci://ghcr.io/nvidia/openshell/helm-chart --version 0.0.0-dev` succeeds in a kind cluster, the gateway pod becomes Ready, port-forward exposes 8080, and the released CLI registers the in-cluster gateway and runs `openshell status` against it. | -`install.sh` defaults to the *latest tagged* release — the canary is therefore checking that the most recent public release still installs, not the just-published `dev` build. The `kubernetes` job is the exception: it pins to `0.0.0-dev` chart + `:dev` images. +`install.sh` defaults to the *latest tagged* release — the `macos`, `ubuntu`, and `fedora` jobs are therefore checking that the most recent public release still installs, not the just-published `dev` build. The jobs that download workflow artifacts (`ubuntu-snap` and `ubuntu-podman-rootless`) test artifacts from the triggering Release Dev run. The `kubernetes` job pins to the `0.0.0-dev` chart + `:dev` images. ## Trigger paths @@ -35,6 +37,10 @@ on: When dispatched manually, `github.event.workflow_run.head_sha` is empty and the workflow falls back to `github.sha` (the branch tip) for the `install.sh` URL. +The `ubuntu-snap` and `ubuntu-podman-rootless` jobs run only for `workflow_run` +events because they need a completed Release Dev run ID for +`actions/download-artifact`. + ## Manual dispatch Run the canary as-is on the current branch: @@ -109,6 +115,8 @@ Loopback registration auto-derives the gateway name to `openshell` if `--name` i |---|---|---| | `macos`/`ubuntu`/`fedora` job fails on `install.sh` | Latest tagged release missing an asset, checksum mismatch, or `install.sh` regression on this branch. | Job log around the `curl … install.sh \| sh` step. | | `macos`/`ubuntu`/`fedora` job fails on `openshell status` | Local gateway service did not start (systemd/brew/podman). Often a driver issue. | Service logs in the job log; `OPENSHELL_DRIVERS` env in the "Ensure …" step. | +| `ubuntu-podman-rootless` fails before `cargo xtask release-smoke-test` | Lima/QEMU installation, KVM access, or Release Dev artifact download failed. | "Install Lima and QEMU", "Cache ~/.cache/lima", and "Download Debian package from release-dev artifacts". | +| `ubuntu-podman-rootless` fails inside `cargo xtask release-smoke-test` | Lima could not boot the VM, rootless Podman setup failed, the `.deb` did not install, the packaged gateway did not start, or sandbox creation/default-deny curl failed. | The xtask guest script emits OpenShell, systemd, Podman, container, and gateway diagnostics on failure. | | `kubernetes` job fails on `helm install --wait` | Chart did not deploy in 5 min — usually image pull failure or readiness probe failing. | "Diagnostics on failure" step dumps `helm status`, manifest, pod describe, pod logs. | | `kubernetes` job fails on `kubectl wait` | Gateway pod stuck `CrashLoopBackOff` or `ImagePullBackOff`. | Diagnostics dump; check `:dev` image existence at `ghcr.io/nvidia/openshell/gateway`. | | `kubernetes` job fails on `openshell gateway add` or `status` | Port-forward not reachable, or CLI/gateway proto mismatch. | `port-forward.log` and `openshell gateway list` in the diagnostics dump. | diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index 2f205fd59c..be0a50fbd6 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -196,6 +196,85 @@ jobs: openshell gateway select snap-docker openshell status + ubuntu-podman-rootless: + name: Ubuntu Rootless Podman VM + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_VERSION: v2026.4.25 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - name: Install mise + run: | + set -euo pipefail + curl https://mise.run | MISE_VERSION="${MISE_VERSION}" sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" + + - name: Install tools + run: mise install --locked + + - name: Install Lima and QEMU + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y \ + lima \ + qemu-system-x86 \ + qemu-utils + if [ -e /dev/kvm ]; then + sudo chmod 666 /dev/kvm + fi + limactl --version + qemu-system-x86_64 --version + + - name: Resolve Lima version + id: lima-version + run: | + set -euo pipefail + version="$(limactl --version | awk '{print $NF}')" + echo "version=${version}" >> "$GITHUB_OUTPUT" + + - name: Cache ~/.cache/lima + uses: actions/cache@v4 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-version.outputs.version }} + + - name: Download Debian package from release-dev artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + pattern: deb-linux-amd64 + path: release/ + merge-multiple: true + + - name: Smoke test Debian package in Lima VM + run: | + set -euo pipefail + deb_path="$(find release -maxdepth 1 -type f -name '*.deb' -print -quit)" + if [ -z "$deb_path" ]; then + echo "::error::No Debian package artifact found in release/" + find release -maxdepth 2 -type f -print || true + exit 1 + fi + mise x -- cargo xtask release-smoke-test \ + --deb "$deb_path" \ + --guest-os ubuntu-26.04 + + - name: Lima diagnostics on failure + if: failure() + run: | + set +e + limactl list + find "${HOME}/.lima" -maxdepth 3 -type f -name '*.log' -print -exec tail -n 120 {} \; + kubernetes: name: Kubernetes Helm (kind) if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}