diff --git a/.config/mise/tasks/rust/consumer b/.config/mise/tasks/rust/consumer index cf7ecbd8..03b9c211 100755 --- a/.config/mise/tasks/rust/consumer +++ b/.config/mise/tasks/rust/consumer @@ -10,7 +10,7 @@ trap 'rm -rf "$(dirname "$consumer")"' EXIT cargo new --quiet "$consumer" cat >>"$consumer/Cargo.toml" <- + cargo build --manifest-path ../../rust/Cargo.toml --locked --release + -p morphir-python-extension --target x86_64-unknown-linux-gnu args: --release --locked --sdist --out ../../dist/native - name: Audit the Linux baseline and bundled libraries @@ -130,6 +143,13 @@ jobs: path: dist/native/ if-no-files-found: error + - name: Upload the process executable from the portable build + uses: actions/upload-artifact@v6 + with: + name: process-linux-x86_64 + path: rust/target/x86_64-unknown-linux-gnu/release/morphir-python-extension + if-no-files-found: error + test: name: Test needs: [changes, native-wheel] @@ -218,9 +238,83 @@ jobs: run: | MORPHIR_NATIVE_WHEEL="$(ls dist/native/*.whl)" mise run install + - name: Download the portable process executable + uses: actions/download-artifact@v7 + with: + name: process-linux-x86_64 + path: dist/process/ + - name: Package and qualify the bundle offline run: mise run extension:bundle + - name: Package and qualify the process guest offline + run: | + mkdir -p .morphir/build/extensions .dev/qualification + uv run --no-sync python -m scripts.process_extension \ + --executable dist/process/morphir-python-extension \ + --output .morphir/build/extensions/python-process \ + --commit "$GITHUB_SHA" \ + --report .dev/qualification/process-x86_64-unknown-linux-gnu.json \ + --host-check + + # Full executable conformance belongs to candidate qualification. The + # normal PR path already covers model negotiation and framing in Rust. + - name: Cache the source-pinned canonical MCK build + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + uses: actions/cache@v5 + with: + path: .dev/mep-tool/ + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- + + - name: Cache the independent WASM adapter build + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + uses: Swatinem/rust-cache@v2 + with: + workspaces: tools/extension-qualification -> ../../.dev/host-qualification-target + + - name: Prepare the shared checker and transparent WASM adapter + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/.dev/host-qualification-target + run: | + python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + cargo build --locked --release --manifest-path tools/extension-qualification/Cargo.toml --bin mep_wasm_host + + - name: Run and independently recheck canonical MEP reports + if: inputs.qualify-mep || github.event_name == 'workflow_dispatch' + run: | + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python-process --runtime process \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --report .dev/qualification/mep-x86_64-unknown-linux-gnu-report.json \ + --receipt .dev/qualification/mep-x86_64-unknown-linux-gnu-recheck.json + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python --runtime wasm \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --adapter .dev/host-qualification-target/release/mep_wasm_host \ + --report .dev/qualification/bundle-mep-report.json \ + --receipt .dev/qualification/bundle-mep-recheck.json + + - name: Retain failed checker build diagnostics + if: failure() && hashFiles('.dev/mep-tool/*/build.log') != '' + uses: actions/upload-artifact@v6 + with: + name: mep-build-log-linux-x86_64 + path: .dev/mep-tool/*/build.log + include-hidden-files: true + + - name: Upload the qualified process bundle and receipt + uses: actions/upload-artifact@v6 + with: + name: morphir-python-process-x86_64-unknown-linux-gnu + path: | + .morphir/build/extensions/python-process/ + .dev/qualification/process-x86_64-unknown-linux-gnu.json + .dev/qualification/mep-x86_64-unknown-linux-gnu-*.json + include-hidden-files: true + if-no-files-found: error + - name: Upload the extension bundle uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4f72300..5f37282d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,20 +10,71 @@ on: # Qualification only, including every native wheel platform. Publishing # requires a tag push. workflow_dispatch: + inputs: + mck-diagnostic: + description: Build only the shared MCK on Windows; never stage or publish a release + type: boolean + default: false permissions: contents: read concurrency: - group: release-${{ github.ref }} + group: ${{ inputs.mck-diagnostic && 'mck-diagnostic' || 'release' }}-${{ github.ref }} cancel-in-progress: false jobs: + mck-diagnostic: + name: Diagnose Windows shared MCK build + if: github.event_name == 'workflow_dispatch' && inputs.mck-diagnostic + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v3 + with: + install_args: rust python + - uses: actions/cache@v5 + with: + path: | + .dev/mep-tool/ + ${{ runner.temp }}/mck + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- + - name: Build the pinned canonical checker + env: + MORPHIR_MCK_CARGO_HOME: ${{ runner.temp }}/mck + run: python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + - name: Inspect checkout changes after the Windows build + if: always() + shell: pwsh + run: | + Get-Item .dev/mep-tool/*/source | ForEach-Object { + $source = $_.FullName + Write-Output "Pinned MCK source: $source" + git -C $source status --short + git -C $source diff --stat + git -C $source diff --ignore-space-at-eol --stat + git -C $source submodule foreach --recursive 'git status --short' + git -C $source submodule foreach --recursive 'git diff --stat' + } + - name: Retain checker setup diagnostics + if: always() + uses: actions/upload-artifact@v6 + with: + name: windows-mck-build-diagnostic + path: | + .dev/mep-tool/*/build.log + .dev/qualification/mep-tool.json + include-hidden-files: true + if-no-files-found: warn + qualify: name: Qualify release + if: inputs.mck-diagnostic != true uses: ./.github/workflows/ci.yml with: release-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} + qualify-mep: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/extension/python/v') }} # Every advertised platform builds its own wheel once; the same job installs # that exact wheel, with the qualified core and tools wheels, into a clean @@ -34,7 +85,8 @@ jobs: if: >- github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/native-v') || - startsWith(github.ref, 'refs/tags/python-v') + startsWith(github.ref, 'refs/tags/python-v') || + startsWith(github.ref, 'refs/tags/extension/python/v') strategy: fail-fast: false matrix: @@ -83,12 +135,18 @@ jobs: uses: PyO3/maturin-action@v1 env: MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }} + CARGO_TARGET_DIR: ${{ github.workspace }}/rust/target with: working-directory: packages/morphir-python-native maturin-version: '1.15.0' rust-toolchain: '1.98.1' target: ${{ matrix.target }} manylinux: ${{ matrix.manylinux || 'off' }} + # Process guests share the wheel's Linux baseline and compiled core. + before-script-linux: >- + ${{ (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) && + format('cargo build --manifest-path ../../rust/Cargo.toml --locked --release -p morphir-python-extension --target {0}', matrix.target) || '' }} args: --release --locked --out ../../dist/native - name: Set up Python 3.14 @@ -159,6 +217,119 @@ jobs: path: dist/mise-install.json if-no-files-found: error + # Linux x86_64 already produced and qualified this exact guest in qualify. + # The other producers use their matching native wheel for API comparison. + - name: Build the process executable on macOS and Windows + if: >- + matrix.manylinux == '' && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.target == 'aarch64-apple-darwin' && '11.0' || '10.12' }} + # The standalone guest must not require the VC redistributable. + # This setting is confined to the executable, not the Python wheel. + RUSTFLAGS: ${{ runner.os == 'Windows' && '-C target-feature=+crt-static' || '' }} + run: >- + cargo +1.98.1 build --manifest-path rust/Cargo.toml --locked --release + -p morphir-python-extension --target '${{ matrix.target }}' + + - name: Refuse external VC and Python runtime dependencies + if: >- + runner.os == 'Windows' && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: pwsh + run: | + ./.github/scripts/check_process_windows.ps1 ` + -Executable rust/target/x86_64-pc-windows-msvc/release/morphir-python-extension.exe ` + -Report .dev/qualification/windows-process-imports.json + + - name: Qualify the process bundle against the matching Python API + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + run: | + uv venv .dev/process-check --python 3.14 + qualifier=.dev/process-check/bin/python + executable=rust/target/${{ matrix.target }}/release/morphir-python-extension + if [[ "${{ runner.os }}" == Windows ]]; then + qualifier=.dev/process-check/Scripts/python.exe + executable="$executable.exe" + fi + uv pip install --python "$qualifier" dist/pure/*.whl dist/native/*.whl + mkdir -p .morphir/build/extensions .dev/qualification + "$qualifier" -m scripts.process_extension \ + --executable "$executable" \ + --output .morphir/build/extensions/python-process \ + --commit "$GITHUB_SHA" \ + --report .dev/qualification/process-${{ matrix.target }}.json \ + --host-check + + - name: Set up the canonical MCK toolchain + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: jdx/mise-action@v3 + with: + install_args: rust python + + - name: Cache the source-pinned canonical MCK build + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: actions/cache@v5 + with: + path: | + .dev/mep-tool/ + ${{ runner.os == 'Windows' && format('{0}/mck', runner.temp) || '' }} + key: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1-${{ hashFiles('rust/extension.toml', 'scripts/mep_tool.py') }} + restore-keys: mep-${{ runner.os }}-${{ runner.arch }}-rust-1.98.1- + + - name: Build the shared checker and qualify process MEP + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + shell: bash + env: + MORPHIR_MCK_CARGO_HOME: ${{ runner.os == 'Windows' && format('{0}/mck', runner.temp) || '' }} + run: | + python -m scripts.mep_tool --output .dev/qualification/mep-tool.json + python -m scripts.qualify_mep \ + --bundle .morphir/build/extensions/python-process --runtime process \ + --tool .dev/qualification/mep-tool.json --commit "$GITHUB_SHA" \ + --report .dev/qualification/mep-${{ matrix.target }}-report.json \ + --receipt .dev/qualification/mep-${{ matrix.target }}-recheck.json + + - name: Retain failed checker build diagnostics + if: failure() && hashFiles('.dev/mep-tool/*/build.log') != '' + uses: actions/upload-artifact@v6 + with: + name: mep-build-log-${{ matrix.target }} + path: .dev/mep-tool/*/build.log + include-hidden-files: true + + - name: Upload the qualified process bundle and receipt + if: >- + !matrix.reuse && + (github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v')) + uses: actions/upload-artifact@v6 + with: + name: morphir-python-process-${{ matrix.target }} + path: | + .morphir/build/extensions/python-process/ + .dev/qualification/process-${{ matrix.target }}.json + .dev/qualification/mep-${{ matrix.target }}-*.json + .dev/qualification/*-process-imports.json + include-hidden-files: true + if-no-files-found: error + - name: Upload the qualified wheel uses: actions/upload-artifact@v6 with: @@ -208,6 +379,104 @@ jobs: path: dist/candidates/ if-no-files-found: error + # Assembly requires all five process and the portable WASM producer's + # canonical MCK run plus saved-report recheck for these exact artifact bytes. + process-candidates: + name: Verify the process release candidate set + needs: [qualify, native-wheels] + if: >- + github.event_name == 'workflow_dispatch' || + startsWith(github.ref, 'refs/tags/extension/python/v') + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Download each qualified process platform + uses: actions/download-artifact@v7 + with: + pattern: morphir-python-process-* + path: dist/process-inputs/ + + - name: Download the qualified WASM reference + uses: actions/download-artifact@v7 + with: + name: morphir-python-extension-bundle + path: dist/wasm-check/ + + - name: Require canonical MEP rechecks and matching installed results + run: | + python -m scripts.process_release \ + --inputs dist/process-inputs \ + --output dist/process-release \ + --commit "$GITHUB_SHA" \ + --wasm-bundle dist/wasm-check/.morphir/build/extensions/python \ + --wasm-report dist/wasm-check/.dev/qualification/bundle-host.json \ + --report dist/process-assembly.json \ + --assets-output dist/process-assets + + - name: Verify runtime switching, installed integrity and platform refusal + run: | + (cd dist/process-assets && sha256sum --strict --check SHA256SUMS) + mkdir -p .dev/process-archive + tar -xzf dist/process-assets/morphir-python-extension-*.tar.gz -C .dev/process-archive + python -m scripts.qualify_runtime \ + --wasm dist/wasm-check/.morphir/build/extensions/python \ + --process .dev/process-archive \ + --report dist/runtime-choice.json + + # Source-pinned host assurance belongs to the existing manual/release + # candidate consumer. Build once; exercise the downloaded guest bytes. + - name: Set up the pinned Rust host toolchain + uses: jdx/mise-action@v3 + with: + install_args: rust python + + - name: Cache the independent Rust host build + uses: Swatinem/rust-cache@v2 + with: + workspaces: tools/extension-qualification -> ../../.dev/host-qualification-target + + - name: Build and check the source-pinned Rust host qualifier + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/.dev/host-qualification-target + run: | + manifest=tools/extension-qualification/Cargo.toml + cargo fmt --manifest-path "$manifest" --check + cargo clippy --locked --release --manifest-path "$manifest" --all-targets --no-deps -- -D warnings + cargo test --locked --release --manifest-path "$manifest" + cargo build --locked --release --manifest-path "$manifest" + + - name: Qualify real host channels and retain repeated timing samples + run: | + wasm=dist/wasm-check/.morphir/build/extensions/python + version="$(python -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + python -m scripts.benchmark_extension \ + --checker .dev/host-qualification-target/release/morphir-python-host-qualification \ + --wasm "$wasm/morphir-python-binding-$version.wasm" \ + --process ".dev/process-archive/morphir-python-extension-$version-x86_64-unknown-linux-gnu" \ + --output dist/host-qualification --samples 5 + + - name: Upload the process candidate and its release evidence + uses: actions/upload-artifact@v6 + with: + name: process-release-candidate + path: | + dist/process-release/ + dist/process-assets/ + dist/process-assembly.json + dist/runtime-choice.json + dist/host-qualification/ + dist/process-inputs/*/.dev/qualification/*.json + dist/wasm-check/.dev/qualification/bundle-mep-*.json + include-hidden-files: true + if-no-files-found: error + publish-native: name: Publish morphir-python-native to PyPI if: >- @@ -239,8 +508,8 @@ jobs: extension-candidate: name: Verify the extension bundle candidate - needs: qualify - if: startsWith(github.ref, 'refs/tags/extension/python/v') + needs: [qualify, native-wheels, process-candidates] + if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/extension/python/v') runs-on: ubuntu-latest steps: - name: Checkout repository @@ -255,17 +524,35 @@ jobs: - name: Refuse a bundle that is not from this tag's commit run: | bundle=dist/bundle-check/.morphir/build/extensions/python + version="$(python3 -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + tag="extension/python/v$version" + if [[ "${{ github.ref_type }}" == tag ]]; then tag="$GITHUB_REF_NAME"; fi python3 scripts/release_candidates.py bundle "$bundle" \ - --tag "${{ github.ref_name }}" --commit "$GITHUB_SHA" + --tag "$tag" --commit "$GITHUB_SHA" mkdir -p dist/bundle - version="${GITHUB_REF_NAME#extension/python/v}" # Release assets keep morphir-rust's descriptor name. cp "$bundle/morphir-python-binding-$version.wasm" \ "$bundle/morphir-python-binding-$version.wasm.sha256" dist/bundle/ cp "$bundle/release.json" "dist/bundle/morphir-python-binding-$version.release.json" cp dist/bundle-check/.dev/qualification/bundle*.json dist/bundle/ - # Seal the final asset names, including the renamed descriptor. - (cd dist/bundle && sha256sum -- morphir-python-binding-* bundle*.json > SHA256SUMS) + + - name: Download the qualified process assets and producer evidence + uses: actions/download-artifact@v7 + with: + name: process-release-candidate + path: dist/process-check/ + + - name: Stage both runtimes with their exact qualification evidence + run: | + (cd dist/process-check/process-assets && sha256sum --strict --check SHA256SUMS) + cp dist/process-check/process-assets/morphir-python-extension-* dist/bundle/ + cp dist/process-check/process-assets/process-assets.json dist/bundle/ + cp dist/process-check/process-assembly.json dist/process-check/runtime-choice.json dist/bundle/ + cp dist/process-check/process-inputs/*/.dev/qualification/*.json dist/bundle/ + version="$(python3 -c 'from scripts.extension_bundle import crate_version; print(crate_version())')" + tar -czf "dist/bundle/extension-host-qualification-$version.tar.gz" -C dist/process-check host-qualification + # Seal the final asset names, including both renamed descriptors. + (cd dist/bundle && sha256sum -- * > SHA256SUMS) - name: Upload the release set uses: actions/upload-artifact@v6 diff --git a/docs/python-extension.md b/docs/python-extension.md index 5ce7d5cb..ebe0d635 100644 --- a/docs/python-extension.md +++ b/docs/python-extension.md @@ -4,8 +4,8 @@ The `morphir-python` extension lets a Morphir host, such as the Rust `morphir` CLI, compile Python with `morphir compile --language python` and generate Python with `morphir generate --target python`. It is the [`morphir-python-binding`](https://github.com/finos/morphir-python/blob/main/rust/crates/morphir-python-binding/README.md) -compiler built as a WebAssembly guest that speaks the Morphir extension protocol -(MEP). It is the same compiler that the `morphir-python` Python package runs +compiler built as a WebAssembly guest or a standalone process speaking the +Morphir extension protocol (MEP). It is the same compiler that the Python package runs natively, so both give the same IR and the same diagnostic codes. This repository builds and releases the extension. Bundles up to @@ -20,7 +20,8 @@ available there. | Language | `python` (`.py`) | | Target | `python` | | IR versions | `3`, `4` | -| MEP version | `0.1` | +| Published 0.4.2 MEP version | `0.1` | +| Development 0.5.0 MEP versions | `0.2.0-draft.1`, `0.1` | | Multiple source documents | `multiDocument: true`, from `0.4.2` | Extension `0.4.2` pairs with `morphir-python==0.1.0` and @@ -29,6 +30,45 @@ The native API reports compiler version `0.4.2`. Use `0.4.2` or later with hosts that require an explicit multi-document claim; `0.4.1` omits that claim. +Development extension `0.5.0` pairs with `morphir-python==0.1.1` and +`morphir-python-native==0.1.2`, using published Rust SDK `0.4.0`. These Python +and extension versions are candidates, not published installation targets. +The bridge API remains revision `1`; the core `morphir` library remains `0.1.0`. + +## Model protocol in the development candidate + +Package, protocol, envelope, claims and IR versions identify different contracts: + +| Contract | Candidate value | +| --- | --- | +| Compiler / extension package | `0.5.0` | +| Rust extension SDK dependency | exactly `0.4.0` from crates.io | +| MEP offers | exact `0.2.0-draft.1` and legacy `0.1` | +| Model envelope | exact `0.1.0-draft.1` | +| Claim set | `0.1.0-draft.2` | +| Model profile / codec | `morphir-ir` / `morphir-json` | +| Semantic IR | `3.0.0` or `4.0.0`, Library distributions | + +The host must offer complete operation/profile/codec/envelope/IR tuples through +`modelTransport` during initialization. The guest selects only their intersection +and the lower value of each limit. Both `morphir.frontend.compileModel` and +`morphir.backend.generateModel` support each stated IR version. The static limits +are 12 MiB encoded content, 8 MiB decoded content, depth 128 and 100,000 nodes. +Session capabilities remain fixed after initialization. Legacy sessions use +`compile`/`generate` and cannot call model methods. Other MEP drafts do not match. + +Numeric and string request IDs retain their type and value. Unsupported Ion +profiles or evidence, required guarantees, and Specs/Application generation are +refused explicitly. No rich-model preservation or automatic codec fallback is +advertised. + +The shared executable MCK's separate `python-json-library-v1` profile qualifies +the supported JSON subset. Its fixed expectations and existing MoonBit rich-Ion +profile remain independent. WASM qualification uses the pinned Rust Extism host +adapter; raw request and response bodies pass unchanged to the guest. This ABI +handles requests only, so notification silence and exit describe the adapter and +guest route, not notification delivery inside the WASM guest. + The declaration is in [`rust/extension.toml`](https://github.com/finos/morphir-python/blob/main/rust/extension.toml). ## Install the qualified release bundle @@ -110,6 +150,94 @@ Use `python3 scripts/docs_hosts.py qualify` to repeat the displayed workflow and offline checks. Receipts record pins, file hashes, host identity, checker and input digest. Download failure never counts as a passing qualification. +## Choose a runtime in development + +Published bundles remain WASM. The process bundle is a development candidate +from the manual release-qualification workflow, not a published installation +channel yet. Its executable is a MEP guest for the finos/morphir host. The +`morphir-python` CLI continues to use its pinned native Python wheel through the +[mise/pipx installation](python-compiler.md); choosing an extension runtime does +not configure that CLI. + +Use two separate canonical bundle directories and named local repositories. +The WASM bundle contains its `.wasm`, checksum and `release.json`. The process +bundle contains the raw executables, their checksums and its own `release.json`. +The canonical repository writer refuses a bundle mixing both runtimes. +The process candidate includes Linux x86_64/ARM64, macOS x86_64/ARM64 and Windows +x86_64. Installation chooses the matching platform and refuses an absent one. + +The `process-release-candidate` workflow artifact also retains `process-assets/`: +five raw executables and checksums, a separately named process descriptor, +`morphir-python-extension-VERSION.tar.gz`, a source-bound packaging receipt and +`SHA256SUMS`. The archive contains the complete canonical process bundle, with +`release.json` and executable permissions. Its payloads match the raw assets. +These are development candidates. GitHub release publication requires matching +independent conformance evidence and the approved release tag. + +After downloading the candidate, verify and extract it in Bash: + +```bash +cd process-assets +shasum -a 256 -c SHA256SUMS +mkdir ../python-process +tar -xzf morphir-python-extension-*.tar.gz -C ../python-process +export PROCESS_BUNDLE="$(cd ../python-process && pwd)" +``` + +The qualification workflow runs the installation and runtime-switching checks +against this extracted archive. Its original WASM bundle remains separate. + +Set `WASM_BUNDLE` and `PROCESS_BUNDLE` to those unmodified directories. With +`MORPHIR_HOST` pointing at the pinned host described above, an isolated trial is: + +```bash +export MORPHIR_HOME="$(mktemp -d)" +VERSION=0.5.0 # Current development candidate version from release.json. +"$MORPHIR_HOST" extension repository init ./wasm-repository +"$MORPHIR_HOST" extension repository init ./process-repository +"$MORPHIR_HOST" extension repository add python-wasm --directory ./wasm-repository +"$MORPHIR_HOST" extension repository add python-process --directory ./process-repository +"$MORPHIR_HOST" extension repository publish python-wasm --bundle "$WASM_BUNDLE" +"$MORPHIR_HOST" extension repository publish python-process --bundle "$PROCESS_BUNDLE" +"$MORPHIR_HOST" extension install morphir-python --repository python-wasm --version "$VERSION" + +# Switch the active provider in this isolated Morphir home. +"$MORPHIR_HOST" extension uninstall morphir-python +"$MORPHIR_HOST" extension install morphir-python --repository python-process --version "$VERSION" +``` + +The repository selects the runtime; this host has no `--runtime` installation +flag. It permits one active `morphir-python` provider per home. Uninstall it +before installing the other runtime from its explicit repository. Compile and +generate commands remain the same. The process guest starts in stdio mode with +no arguments; `--stdio` is also accepted by hosts configured to pass it. +Installation restores executable permissions, so the Morphir host can launch a +downloaded raw executable without a user Rust toolchain or Python interpreter. +A process has ordinary OS rights; WASM uses the host's Extism isolation. + +The developer [Rust host checker](https://github.com/finos/morphir-python/tree/main/tools/extension-qualification) +pins the upstream host source and tests actual WASM/process bytes independently +of the installation recipe. It verifies legacy v3/v4 operations, diagnostic +parity, orderly shutdown and process timeout cleanup. Its timing runner retains +fresh-guest startup and compile/generate samples for small and larger inputs, +including the machine, build profile, input and artifact identities. These +observations do not establish newer-draft conformance or a general speed claim. + +The release-assembly check runs this switch in both directions with fresh +project caches, compares v3/v4 IR and generated-file digests, rejects an actual +foreign-only bundle, and checks that changing installed bytes causes a digest +refusal before compilation. Each platform's own bundle qualifier also checks +installed-byte integrity. These are legacy-route checks. The separate executable +MCK qualification covers the new MEP draft and its model operations. Promotion +must retain and verify both kinds of evidence for the candidate being released. + +To verify already built candidates without rebuilding them: + +```bash +python -m scripts.qualify_runtime --wasm "$WASM_BUNDLE" \ + --process "$PROCESS_BUNDLE" --report .dev/qualification/runtime-choice.json +``` + ## Build a development bundle From the repository root: @@ -133,7 +261,9 @@ This task: deletes the repository, blocks network access, and compiles, generates and recompiles a two-module model in IR v3 and v4; 4. checks that the IR from the extension equals the IR from the Python API for - the same sources. + the same sources; +5. requires digest refusal when installed bytes are changed in a fresh project, + then restores the original artifact. The bundle is written to `.morphir/build/extensions/python/`, and JSON receipts with every digest are written to `.dev/qualification/`. A bundle built from a diff --git a/docs/releasing.md b/docs/releasing.md index 6bcdb77f..626a0267 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,4 +1,4 @@ -# Release Python packages and the WASM extension +# Release Python packages and extensions The package versions are independent. A maintainer approves tags and publication. The [Release workflow](https://github.com/finos/morphir-python/actions/workflows/release.yml) @@ -10,7 +10,7 @@ candidate, including the platform matrix, without publishing a release. | `morphir-v` | Core wheel and source archive on PyPI and GitHub Releases | Python package installer | | `native-v` | Five native compiler wheels and a source archive on PyPI | Exact dependency of the CLI | | `python-v` | CLI wheel and source archive on PyPI and GitHub Releases | mise `pypi:`/`pipx:`, uv or pip | -| `extension/python/v` | WASM, SHA-256 checksum, release descriptor, qualification reports and `SHA256SUMS` on GitHub Releases | Morphir extension host | +| `extension/python/v` | WASM and five process executables, separate runtime descriptors, process bundle archive, qualification evidence and `SHA256SUMS` on GitHub Releases | Morphir extension host | The CLI release is a Python distribution. mise installs it from PyPI into an isolated environment; GitHub release assets are distribution mirrors, not @@ -58,6 +58,54 @@ The WASM gate checks the tag's compiler version, source commit, descriptor diges and checksum contents. Publication rechecks `SHA256SUMS` for the final release asset names, including the renamed release descriptor. +## Qualify both extension runtimes + +The manual and extension-tag workflows run the shared executable MCK on each +process producer and once for the portable WASM guest. `rust/extension.toml` +pins the merged upstream source, tree, lockfile, Python JSON corpus and complete +91-case identity set. `scripts.mep_tool` verifies that source and builds its CLI; +it never replaces upstream expectations. Cargo may reuse compatible cached +objects. Each report names an immutable copy of the built checker. +The published host CLI and this source-built checker can have the same package +version. Use the pinned source commit for the Python JSON profile; the older +published CLI does not contain that profile. + +`scripts.qualify_mep` runs `morphir mck mep guest-run`, then `report-check` with +the original checker and all identity files still available. A failed run or +recheck prevents the producer from uploading a qualified candidate. The WASM +adapter forwards raw request/response bytes to the guest. Its notification and +exit checks cover the adapter/guest route, as documented in the +[extension guide](python-extension.md). + +The collector consumes only successful producers in the same workflow run. It +verifies source, artifact, checker definitions, lockfiles, corpus, all required +case identities, discovery claims, raw report hashes and the successful saved +recheck receipt. Windows and macOS paths remain unchanged. This is a trusted +producer handoff, not a second filesystem recheck on Linux. A standalone report +with `qualified: true` is insufficient. + +Missing, stale, failed or mismatched evidence blocks process assembly and the +dependent extension promotion job. The candidate also checks installed-byte +integrity, full diagnostic parity with the matching Python API, Windows runtime +imports, extracted-archive runtime switching, and real Rust host behavior. The +final release retains raw MCK reports and recheck receipts, assembly and runtime +receipts, and the host timing evidence. Publication verifies final checksums. +Historical WASM releases remain available with their original bytes. + +## Investigate Windows checker setup + +Dispatch the Release workflow with `mck-diagnostic=true` to build only the shared +checker on Windows and retain its complete build log, tool receipt and checkout +status. This mode skips candidate qualification and publication. Leave the input +false for a full release qualification run. + +The Windows checker uses a short Cargo cache under `RUNNER_TEMP/mck` so locked +Git dependencies fit Windows path limits. The override applies only to checker +compilation. Both the parent checkout and initialized Rust submodule use local +Git settings for LF content and long paths. Preparation requires a clean source +tree before and after building; it refuses genuine source edits without +resetting them. These settings preserve the pinned source, lockfile and corpus. + ## Reproduce the installer checks Check the published configurations without changing your global mise setup: diff --git a/openspec/changes/add-process-mep-extension/design.html b/openspec/changes/add-process-mep-extension/design.html index 55297801..109387eb 100644 --- a/openspec/changes/add-process-mep-extension/design.html +++ b/openspec/changes/add-process-mep-extension/design.html @@ -22,14 +22,33 @@

add-process-mep-extension

-

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Proposed; no process or new-draft implementation

- -

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The published SDK 0.3.0 exposes NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type still uses a numeric u64 ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The Python implementation does not opt into model transport and its manifest declares MEP 0.1 only. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

+

OpenSpec change · Bead morphir-5hxy · 2026-10-28 · Implementation qualified; landing and component publication pending

+ +

Context

See proposal.md for motivation and the two spec deltas for requirements. PythonExtension already implements native Rust frontend/backend traits. The baseline published SDK 0.3.0 exposed NativeExtension::frontend_backend, open_protocol, model methods and admission helpers. Its wire request type used a numeric ID, while the canonical contract admits numeric and string IDs; adopting helpers alone is not full draft conformance. The baseline Python0.4.2 implementation declared legacy MEP0.1 only. Candidate0.5.0 uses publishedSDK 0.4.0 and opts into the exact JSON model draft after native/WASM executable qualification. finos/morphir c552dac801f23f2b2b566d29e08780723498ce36 defines exact MEP 0.2.0-draft.1 and model envelope 0.1.0-draft.1. These versions are independent of SDK 0.3.0, compiler 0.4.2, claims format and IR version.

Goals and non-goals

Add a native Rust process guest and current-draft model adapters while retaining WASM and legacy behavior. Keep language semantics shared. Initial model tuples use morphir-ir/morphir-json and supported Library IR 3.0.0 and draft 4.0.0. Rich Ion profiles, unsupported distribution kinds, additional Python syntax, cancellation/progress and performance claims are not introduced. A Python CLI stdio wrapper can follow separately; the selected CLI installation remains mise/pipx.

-

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with --stdio locked as an argument. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

-

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.
-

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

The current proposal does not change protocol claims or release a guest. Every process task remains unstarted.

-

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk 0.3.0: native.rs, traits.rs and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

+

Decisions

RNT-1. Add a thin native Rust guest

Add a process guest crate/entry point, depending on the maintained compiler and published SDK, with no PyO3 or CPython requirement. The proposed command is:

morphir-python-extension --stdio

No-argument launches also select stdio, matching the canonical native bundler and installed process launcher. Reuse the SDK's session endpoint and dispatch. Keep model adaptation in the shared PythonExtension so native, WASM and process paths agree. Put stdio effects in the guest layer. A Python CLI wrapper would reuse the engine but retain an interpreter/launcher dependency; a frozen Python CLI would bundle an unnecessary runtime for this guest.

BoundaryCurrent releaseProposed change
MEPLegacy 0.1Retain 0.1 and add exact 0.2.0-draft.1
Model envelopeNot advertised0.1.0-draft.1, admitted JSON profile
Extension runtimeWASMWASM and native process
SDK0.3.0, numeric-only wire IDsQualified published pin covering the canonical wire contract
Semantic IRSupported v3 and draft v4 Library modelsSame semantic scope, explicit admitted tuples

SDK-1. Resolve concrete SDK wire gaps before claiming conformance

Pin a published SDK that supports canonical numeric and string request IDs and the required draft behavior. SDK 0.3.0 currently has numeric-only ExtensionRequest.id, so link and qualify an upstream SDK update if no suitable release exists. Do not vendor a sibling checkout or silently coerce wire IDs. Both WASM and process must pass the same ID-preservation vectors before the new draft is advertised. The concrete prerequisite is request ID support, issue 316, tracked as morphir-4wrx. SDK 0.4.0 is merged in PR 317 as d51daa8aea7ae09ae295ff254d72b27770c64104. All CI and the committed-source publication dry run pass. The maintainer approved publication at that exact merge. Tag crates/morphir-extension-sdk/v0.4.0 triggered successful workflow40695514380; Cargo independently downloaded the published0.4.0 crate. The compiler now pins registry dependency =0.4.0, without a sibling or Git SDK dependency. Related upstream work is MEP protocol type generation, issue 241; its existence is not evidence that the wire gap is fixed.

WIN-1. Keep the standalone Windows runtime independent of Python

Binary inspection of the b38e00e development executable found VCRUNTIME140.dll. Build only the standalone guest with -C target-feature=+crt-static; preserve the Python wheel's existing ABI configuration. The Windows producer uses MSVC dumpbin /DEPENDENTS, records exact artifact/tool/checker identities, and refuses VC or Python runtime DLL imports. Assembly requires that source-bound proof. This CRT configuration needs separate code generation from the Python wheel; it is not a compatible cache consumer.

MEP-1. Negotiate the new draft explicitly

Retain legacy 0.1 operations and exact 0.2.0-draft.1 offers. Opt in only after supported model methods and executable conformance pass. Reuse SDK model admission and session rules; method constants alone are not support. Never enable model methods under legacy negotiation. Keep optional flags false until implemented.

MOD-1. Start with the explicit JSON model profile

Implement compile_model/generate_model over the existing compile/generate core. Admit complete operation/profile/codec/envelope/IR tuples and their bilateral limits. The initial profile is morphir-ir/morphir-json with envelope 0.1.0-draft.1. Use the canonical base64 payload, exact byte length/hash and critical-field rules. After SDK admission, materialize JSON through published morphir-json 0.2.0 with finite depth/node/string budgets, preserving numeric lexemes and literal private-looking keys. Bound outgoing JSON before extending payload buffers. Reject unadvertised Ion profiles, guarantees and unsupported semantic forms. This achieves bounded new-draft support without implying rich Ion preservation or silently converting rich input.

IO-1. Treat framing and lifetime as a bounded server boundary

Use Content-Length JSON-RPC framing, byte-based lengths, an 8 KiB header ceiling and an initial 16 MiB complete-body ceiling. Advertised decoded/encoded model limits must fit those outer bounds, including envelope overhead. Enforce local bounds even if the peer advertises more. Check partial input, duplicate lengths, integer overflow, invalid UTF-8/JSON and EOF before unbounded allocation. Reserve stdout for frames and flush responses. Follow initialization, capability locks, shutdown and exit; do not respond to notifications. Host supervision owns deadlines and descendant cleanup.

RLS-1. Qualify dual-runtime distribution

Retain the portable WASM guest and publish native process executables for the advertised targets, with canonical empty arguments selecting stdio by default. Use a canonical host-readable descriptor with runtime/platform/mode/digest/claims bound per artifact. Validate the staged descriptor with pinned host tooling; do not assume that either the existing WASM-only packager or native-only bundle command can package a mixed release unchanged. Preserve historical assets. Release exact native/CLI package pairs if shared compiler changes affect them.

HST-1. Qualify hosts independently

The Rust host provides ProcessLaunch/ProcessChannel and verified-byte staging. It clears the child environment, favoring a directly executable Rust guest. Qualify a pinned conforming Rust host and test each documented MoonBit route independently. Use a pinned source build when a released host lacks the new draft, and state that prerequisite. If a route needs upstream work, record that dependency and label it unsupported until it passes. Existing MoonBit process wrappers demonstrate framing, not Python extension activation.

+

Risks and trade-offs

RiskMitigation
Draft, SDK and runtime support are confusedExact independent version table, live negotiation and candidate-bound conformance before claims.
Model content or guarantees are lostAdmit only explicit JSON tuples initially. Refuse unsupported profiles, critical fields and guarantees; no silent fallback.
Malformed input exhausts resourcesBound headers, wire bodies and decoded bytes before allocation; test boundary and partial cases.
Process mode is mistaken for a sandboxDocument ordinary OS privileges and the host's explicit supervision, distinct from WASM isolation.
Packaging or host support lagsPin descriptor readers and host builds, qualify installed bytes, record upstream blockers and make only verified support claims.
Native execution is assumed fasterMeasure startup and representative compile/generate workloads before making a performance claim.

The baseline executable MCK accepted only moonbit-model-bool-library-v1, with mandatory rich Ion semantics and MoonBit artifact inventories. Python cannot qualify against that profile. The maintainer approved a separate Python JSON profile, merged in upstreamPR1109 as85cf4352eeb517fefb85cda10961f749cce51004 and tracked by closed morphir-av8s. Its treec42c55ef3aa9d0190850251dac97bd094cebb4c4 equals the independently audited328c0216 source tree. The source, lockfile, corpus and complete91-case identity set are pinned in rust/extension.toml. Existing MoonBit rich-Ion checks remain unchanged. The clean committed328c0216 shared checker independently qualified all91observations for both actual release-native and WASM0.5.0 bytes, and saved report-check passed for both. Review corrected two non-isolated negative vectors; reports now reach invalid_base64 and unsupported_ir_version. The WASM bridge passes raw request/response bytes; notification silence and exit qualify the adapter/guest route because Extism exposes a requests-only handle ABI. The merged85cf4352 checker also passes both actual runtime candidates and saved rechecks. Release producers now run the canonical suite and saved recheck before upload. No unit-test result substitutes for the executable suite; the replacement five-platform candidate run 40726356123 now passes.

+

Migration and rollout

  1. Add negotiation and bounded JSON model adapters with tests and compatibility documentation.
  2. Add the Rust process server with framing and lifecycle tests; preserve the existing WASM path.
  3. Build and qualify runtime artifacts and descriptors with matching claims and source receipts.
  4. Run pinned MEP guest conformance and real host installation/operation cases, including legacy hosts and explicit refusals.
  5. After maintainer approval, publish a new extension version and document runtime selection. Existing users can stay on the prior WASM release; never replace its bytes.

Implementation is underway on the process delivery branch. The0.5.0 candidate advertises exact0.2.0-draft.1 and legacy0.1 after publishedSDK adoption and passing independent native/WASM reports. Extension0.4.2 remains the published legacy release. All five process platforms, portable WASM and final candidate promotion pass in run 40726356123 at bce5d85. Component publication remains subject to exact maintainer approval.

+

Build and release assessment

+

The personal build-master skill applies because this repository has no local copy. Process delivery belongs to the existing compiler artifact producers. Build the Linux x86_64 executable in the wheel producer's manylinux 2.28 container and qualify it beside WASM in the existing bundle job. The release/manual five-platform wheel matrix owns the remaining native builds, using its matching wheel for installed Python API comparison. A dependent read-only assembly job requires all five byte-bound installed-host and executable MCK receipts plus the WASM reference and its MCK receipt. The existing extension promotion job stages both runtime asset sets and their evidence; only an approved extension tag triggers publication. Reuse the already qualified Linux x86_64 process artifact. No new scheduled workflow or separate platform matrix is introduced.

+ + + + +
Risk and deadlineExecution and ownerEvidence and failure response
Incorrect shared compiler or process bytes, before mergeExisting Linux PR bundle category, repository maintainersCanonical bundle probe, isolated installation, deleted source repository, offline v3/v4 comparison with the exact native wheel; failure blocks the selected aggregate.
Platform-specific executable or wheel failure, before promotionExisting five-platform release matrix, explicit manual qualification and extension tag eventsNative target and compiler/toolchain identity, exact digest/claims/source receipts and installed host comparisons. Missing, failed or skipped selected producers block extension publication through its needs graph.
New protocol claim without independent proof, before promotionMEP conformance category, maintainer-owned prerequisiteSDK 0.4.0 is published and exact-pinned. The Python profile inPR1109 passes actual native/WASM qualification; merged-source pin is recorded; producers run canonical guest-run and saved report-check with all original identities available. Platform packaging does not establish draft qualification. Assembly verifies the successful same-workflow producer recheck, exact report/artifact/source/checker/lock/corpus identities, all91case identities and staged discovery claims. It preserves Windows/macOS paths and does not claim a Linux filesystem recheck of foreign paths. Missing, stale, failed or mismatched evidence blocks the dependent promotion job.
+

The existing Maturin action provides before-script-linux; its pinned Rust toolchain and manylinux selection apply to that hook. Maturin builds the CI wheel from an isolated source archive, so a hook alone did not share registry dependency objects. Run 40672914229 showed two compilations: 3m07s for the process guest and 2m57s for the archive wheel; the whole producer took 7m07s, compared with 2m57s for earlier job 122760321213. An explicit Cargo target directory now allows compatible registry objects to be reused across those source paths while retaining the independent archive build. The replacement b38e00e Linux producer took 5m01s versus 7m07s, and the two reported compile durations changed from 188/194 seconds to 188/58 seconds. This single observation does not establish a universal speedup. Local path-dependent compiler objects may still rebuild because their source identity differs. macOS and Windows use the same native target and deployment configuration as their wheel producer. Cached CLI bytes are checked against the digest-verified archive before execution. Host receipts retain normalized IR and generated artifact digests, actual CLI and qualification-script identity. Byte-bound lockfiles and script sources use LF on every checkout platform. Artifact ZIP transfer restores only the declared executable mode; payload digests and the original producer mode remain checked. The canonical Morphir CLI owns descriptor generation and live probing; the project helper adds the canonical source commit member and writes a separate byte-bound receipt. WASM and process retain separate descriptors because canonical readers refuse mixed-runtime bundles.

+ + + + + + + +
CriterionWeightCurrent assessment
Risk and coverage fit253: focused refusals, installed macOS checks and the b38e00e five-platform matrix pass; replacement source 820c9d0 also passes Windows static-CRT and installed-integrity proof.
Feedback and placement203: one Linux PR path; broader work stays manual/release. Added CI latency not yet measured.
Release evidence and trust204: all six canonical producer/rechecks and final promotion pass; actual checksums, archive payloads/modes and transferred evidence are independently verified.
Execution efficiency153: compatible target reuse reduced one observed Linux producer from 7m07s to 5m01s; platform-specific CRT objects remain separate.
Ownership and structure104: existing bundle and native-wheel categories, explicit needs, repository maintainers own failures.
Operability and recovery104: source/path setup failures are diagnosed and fixed; missing, stale, altered and incomplete evidence is refused; full platform transfer and promotion pass.
+

The b38e00e development candidate passed all five platform producers and process-only assembly in run 40676766327. Cached-tool identity, IR and generated-artifact digests agreed with WASM/API references. The verified CLI recipe selects separate named repositories, with uninstall before changing the active runtime. A real macOS check switched WASM/process/WASM, rejected a foreign-only descriptor and refused changed installed bytes. Integrity checks now run inside every platform producer with fresh project caches and exact expected/observed hashes; atomic replacement avoids modifying hardlinked inputs. These checks remain distinct from the pending new-draft MCK prerequisite. Replacement source 820c9d0 passed all five producers and assembly in run 40685569455. Its Windows dependency proof admits only OS DLLs, with no VC or Python runtime imports. All eleven combined release files, the five producer receipts and the Windows checker proof were independently matched to the collected assembly hashes. Runtime switching, installed-byte tampering and foreign-platform refusal passed on the assembled Linux candidate.

The current candidate supplies the previously missing platform, conformance, transfer and promotion evidence. The assessment concerns implementation assurance; it is not publication authorization or a general performance result. Per-tag qualification still gates publication at the exact approved source. No native speed claim follows from this build design.

+

Windows checker setup and source integrity

The full candidate run exposed Cargo/libgit2 failing on a locked Gleam snapshot path of 265 characters under the default Windows Cargo home. A short checker-only cache under RUNNER_TEMP/mck reduces the longest pinned path to 252 characters. A second diagnostic showed that transient parent Git options did not persist into the Rust submodule: inaccessible long fixtures produced false deleted/modified statuses. Preparation now configures LF and long-path handling locally in each checkout and requires clean source before and after compilation. An actual tracked-file edit is still refused and preserved. No source patch, ignored diff, changed corpus or reduced platform requirement was accepted.

Diagnostic run 40726235322 passes with clean parent/submodule state and a prepared Windows checker receipt. The manual mck-diagnostic mode builds only that checker and retains raw compiler and Git diagnostics; it cannot qualify or publish a release. Its separate concurrency group permits diagnosis while the full platform run continues. The normal workflow remains the promotion authority. The first Linux preparation of the shared checker and WASM adapter took 12m24s, followed by 1m31s for canonical execution and saved rechecks; these are assurance costs, not guest performance measurements.

Download assets and transformed-byte checks

The assembly category stages all five raw executables with checksums, a separately named process descriptor, and one deterministic gzip/tar download containing the canonical eleven-file process bundle. Archive metadata uses fixed ownership/timestamps and explicit executable permissions; every copied and archived payload must retain its original digest. Corrupt transfers, mixed source/claims/platforms, symlinks and extra files are refused without leaving partial assets. The candidate receipt and final checksum list bind the download bytes to assembly. The existing runtime-selection and source-pinned host checks consume the extracted archive, preserving the build-once contract. Actual 820c9d0 archive bytes pass local offline switching, installed-byte refusal and unsupported-platform refusal. The same manual/extension-tag workflow now requires canonical producer MCK rechecks before assembly. Final promotion copies verified raw process assets, their separate descriptor/archive, WASM assets and all producer evidence, then seals their final names. Manual runs stage this exact release set without publishing. An approved extension tag publishes it after all selected jobs pass.

Source-pinned host and timing observations

The standalone developer checker in tools/extension-qualification/ pins Rust host source d51daa8aea7ae09ae295ff254d72b27770c64104, independently of the compiler's published SDK dependency. Actual b38e00e WASM and macOS process bytes pass legacy initialization, v3/v4 type/value compile-generate-recompile, diagnostic parity, orderly close and a real timeout with child reaping. This is not executable MCK or new-draft qualification.

A release-built checker ran five fresh samples per workload on one macOS ARM64 machine. The small workload has one record and one function; the medium workload has 200 records and 100 typed conditional functions. Raw results, input files, exact artifact/checker/lock/toolchain hashes and descriptive timings are retained in .dev/qualification/timings-b38-release/. Startup includes fresh guest initialization and WASM module compilation. OS caches are not cleared; later v3/v4 operations share the session and WASM runs first. No general speed claim follows. The existing manual/release candidate consumer now owns source-pinned host checks and raw timing retention; it builds the checker once and consumes the already qualified guest bytes. The extension promotion job depends on that consumer. Estimated uncached Linux host build cost is 3 to 6 minutes; the observed local release build took 53 seconds. Actual CI timing remains to be measured.

+

Qualification result

All 16 implementation tasks are complete. Full candidate run 40726356123 succeeds at source bce5d859b524edf452b3031e2e2e56fc32f4da71. Later documentation, task and spec-sync edits preserve the production code and check definitions. Publication remains pending exact maintainer approval and per-tag validation.

ObligationObserved evidence
Platform and API agreementAll five process producers and WASM pass installed offline v3/v4 operation checks. Full process diagnostic records match native Python API 0.1.2 with compiler 0.5.0. Windows runtime imports pass.
Canonical current/legacy MEPSix reports each contain 91 required observations with successful saved rechecks from clean merged85cf4352 source, totaling 546. Local transfer verification preserves original paths and exact source/tool/artifact/claim identities.
Assembly and download formFive raw process targets combine into an eleven-file canonical bundle and deterministic archive. Extracted-archive switching, installed-byte tampering and foreign-platform refusal pass. An actual four-platform input set is refused.
Real hosts and timingsPinned Rust host d51daa8 passes actual WASM/process legacy lifecycle, v3/v4 operations, diagnostics, timeout and child cleanup. Ten raw samples, five per small/medium workload, retain inputs and exact toolchain/artifact identities. No general speed claim.
Promotion and retentionFinal staging has 39 assets plus SHA256SUMS. Every final checksum, six raw report/recheck pairs and all eleven archive payload/mode entries are independently checked. Historical 0.4.0/0.4.2 WASM assets remain uploaded with their original digests.

Local validation passes 2632 tests with five intentional skips, strict typing, BDD, focused evidence/assembly tests, actionlint, site checks and strict OpenSpec validation. SDK 0.4.0 is published; Python/native/extension tags have not yet been published. The OpenSpec change remains active until that operational follow-up is complete.

+

Repository evidence and references

  • rust/crates/morphir-python-binding/src/lib.rs: shared PythonExtension traits and truthful current capabilities.
  • rust/crates/morphir-python-native/src/lib.rs: current private two-function JSON bridge; it is not a full MEP transport.
  • Published morphir-extension-sdk0.4.0: canonical request IDs, native session dispatch and model admission helpers.
  • finos/morphir-rust 26d5e707e5ec94d16421db6380edbdc865d85c04: process launch/channel and stdio frontend fixture. Fixture framing must not be copied as a hardened server.
  • finos/morphir-moonbit bd3adc52e6b8f6368fdaee50f76535799732f8d6: portable MEP embedding wrapper. No claim of Python host compatibility follows.
  • Canonical MEP contract and model transport rules.

Related work: morphir-bj9r owns the selected CLI installation route; morphir-ukvn.5.98 tracks the earlier MoonBit extension qualification context.

diff --git a/openspec/changes/add-process-mep-extension/proposal.md b/openspec/changes/add-process-mep-extension/proposal.md index 9bb7cf18..5702107f 100644 --- a/openspec/changes/add-process-mep-extension/proposal.md +++ b/openspec/changes/add-process-mep-extension/proposal.md @@ -11,7 +11,6 @@ The Python compiler currently exposes a WASM MEP guest advertising legacy `0.1`, - Add exact `0.2.0-draft.1` negotiation and bilateral model transport admission, initially for the `morphir-ir`/`morphir-json` profile with the compiler's supported IR v3/v4 Library models. - Implement bounded, integrity-checked model envelopes and compileModel/generateModel adapters for the admitted tuples. Refuse unsupported profiles and guarantees explicitly; rich Ion transport is not advertised by this initial increment. - Publish and qualify both runtime choices, with accurate platform/runtime descriptors and host compatibility documentation. Qualify each documented host route independently. -- Keep this change proposed: no process guest or newer-MEP implementation has been added by PR #52. ## Capabilities @@ -26,6 +25,7 @@ The Python compiler currently exposes a WASM MEP guest advertising legacy `0.1`, ## Beads - **Tracked by**: `morphir-5hxy` +- **Prerequisites**: `morphir-4wrx` (published SDK0.4.0, completed), `morphir-av8s` (approved separate upstream Python JSON executable MCK profile, finos/morphir#1109). - **Related**: `morphir-bj9r` (mise/pipx CLI installation and release assurance), `morphir-ukvn.5.98` (MoonBit Extism qualification context). ## Impact diff --git a/openspec/changes/add-process-mep-extension/tasks.md b/openspec/changes/add-process-mep-extension/tasks.md index 3d78eb74..6e1a880d 100644 --- a/openspec/changes/add-process-mep-extension/tasks.md +++ b/openspec/changes/add-process-mep-extension/tasks.md @@ -1,39 +1,43 @@ # Tasks -This change is proposed. No process guest or newer-MEP implementation has started. +This change is being applied. SDK 0.4.0 is published and the separate upstream Python JSON MCK profile is merged. All implementation and candidate qualification tasks are complete. Landing and approved component publication remain in progress. ## 1. Negotiate current and legacy MEP (morphir-5hxy.1) -- [ ] 1.1 Resolve the SDK 0.3.0 numeric-only wire ID gap with a qualified published SDK pin, linking an upstream prerequisite if needed; add explicit legacy `0.1` and exact `0.2.0-draft.1` offers using the pinned SDK's session helpers; verify numeric/string ID preservation, both handshakes, incompatible drafts and unchanged legacy compile/generate behavior. -- [ ] 1.2 Add bilateral complete-tuple and limit negotiation without widening static claims; verify cross-product, unadvertised method, unsupported guarantee and legacy-session refusals. -- [ ] 1.3 Document independent MEP, envelope, IR, claims and package versions; verify the capability reference matches live discovery and initialized sessions. +- [x] 1.1 Resolve the SDK 0.3.0 numeric-only wire ID gap with a qualified published SDK pin, linking an upstream prerequisite if needed; add explicit legacy `0.1` and exact `0.2.0-draft.1` offers using the pinned SDK's session helpers; verify numeric/string ID preservation, both handshakes, incompatible drafts and unchanged legacy compile/generate behavior. +- [x] 1.2 Add bilateral complete-tuple and limit negotiation without widening static claims; verify cross-product, unadvertised method, unsupported guarantee and legacy-session refusals. +- [x] 1.3 Document independent MEP, envelope, IR, claims and package versions; verify the capability reference matches live discovery and initialized sessions. ## 2. Adapt supported model envelopes (morphir-5hxy.2) -- [ ] 2.1 Implement bounded canonical JSON model-envelope admission using SDK helpers; verify base64, byte length, hash, critical fields, semantic IR version and size-limit negative cases. -- [ ] 2.2 Adapt compileModel/generateModel for admitted JSON-profile v3/v4 Library tuples through the shared compiler; verify compile/generate/recompile semantics and diagnostic parity with legacy operations. -- [ ] 2.3 Refuse unadvertised Ion profiles, unsupported distributions and guarantees without lossy fallback; test these refusals and document the initial support matrix. +- [x] 2.1 Implement bounded canonical JSON model-envelope admission using SDK helpers; verify base64, byte length, hash, critical fields, semantic IR version and size-limit negative cases. +- [x] 2.2 Adapt compileModel/generateModel for admitted JSON-profile v3/v4 Library tuples through the shared compiler; verify compile/generate/recompile semantics and diagnostic parity with legacy operations. +- [x] 2.3 Refuse unadvertised Ion profiles, unsupported distributions and guarantees without lossy fallback; test these refusals and document the initial support matrix. ## 3. Serve native stdio sessions (morphir-5hxy.3) -- [ ] 3.1 Add the thin Rust process guest and documented `morphir-python-extension --stdio` entry point; verify it builds and executes without CPython/PyO3 or a user Rust toolchain. -- [ ] 3.2 Implement bounded Content-Length framing around the SDK session; test Unicode byte lengths, partial/truncated messages, duplicate/invalid lengths, overflow, invalid UTF-8/JSON and header/body limits. -- [ ] 3.3 Preserve request IDs, notification silence, fixed session capabilities, shutdown/exit and protocol-only stdout; verify a real multi-request process test and document stderr and supervision behavior. +- [x] 3.1 Add the thin Rust process guest and documented `morphir-python-extension --stdio` entry point; verify it builds and executes without CPython/PyO3 or a user Rust toolchain. +- [x] 3.2 Implement bounded Content-Length framing around the SDK session; test Unicode byte lengths, partial/truncated messages, duplicate/invalid lengths, overflow, invalid UTF-8/JSON and header/body limits. +- [x] 3.3 Preserve request IDs, notification silence, fixed session capabilities, shutdown/exit and protocol-only stdout; verify a real multi-request process test and document stderr and supervision behavior. ## 4. Distribute both runtime choices (morphir-5hxy.4) -- [ ] 4.1 Build process artifacts for each advertised platform and retain the WASM artifact; verify both use the shared compiler and produce matching supported results and diagnostics. -- [ ] 4.2 Stage canonical runtime/platform/mode/argument/digest/claim descriptors and source receipts with pinned tooling; verify host readers, installed-byte integrity, offline activation and explicit unsupported-platform refusal. -- [ ] 4.3 Add release assurance and runtime-selection documentation alongside each artifact producer, including exact native/CLI pairing where affected; verify missing or mismatched evidence blocks promotion and historical WASM assets remain available. +- [x] 4.1 Build process artifacts for each advertised platform and retain the WASM artifact; verify both use the shared compiler and produce matching supported results and diagnostics. +- [x] 4.2 Stage canonical runtime/platform/mode/argument/digest/claim descriptors and source receipts with pinned tooling; verify host readers, installed-byte integrity, offline activation and explicit unsupported-platform refusal. +- [x] 4.3 Add release assurance and runtime-selection documentation alongside each artifact producer, including exact native/CLI pairing where affected; verify missing or mismatched evidence blocks promotion and historical WASM assets remain available. ## 5. Qualify host interoperability (morphir-5hxy.5) -- [ ] 5.1 Run the pinned executable MEP guest suite for both runtimes and protocol offers, including supported model operations and negative admission; retain source/artifact-bound conformance receipts. -- [ ] 5.2 Qualify the concrete Rust host process and WASM routes using a pinned compatible release or source build; verify initialization, v3/v4 operations, parity, timeout and child cleanup with a clean child environment. -- [ ] 5.3 Test documented MoonBit routes independently, record prerequisite upstream work for any unsupported path and publish only evidenced compatibility claims; verify documentation recipes against the specified host revisions. -- [ ] 5.4 Measure representative cold-start and compile/generate workloads for process and WASM without making an unmeasured speed claim; retain the reproducible inputs, toolchain and timing report. +- [x] 5.1 Run the pinned executable MEP guest suite for both runtimes and protocol offers, including supported model operations and negative admission; retain source/artifact-bound conformance receipts. +- [x] 5.2 Qualify the concrete Rust host process and WASM routes using a pinned compatible release or source build; verify initialization, v3/v4 operations, parity, timeout and child cleanup with a clean child environment. +- [x] 5.3 Test documented MoonBit routes independently, record prerequisite upstream work for any unsupported path and publish only evidenced compatibility claims; verify documentation recipes against the specified host revisions. +- [x] 5.4 Measure representative cold-start and compile/generate workloads for process and WASM without making an unmeasured speed claim; retain the reproducible inputs, toolchain and timing report. ## Workflow follow-up -- Implementation requires separately applying this proposal after review. -- Merge, tags and publication require maintainer approval; sync/archive only after implementation and qualification are complete. +- All 16 implementation tasks are complete. Full candidate [40726356123](https://github.com/finos/morphir-python/actions/runs/40726356123) succeeded at `bce5d859b524edf452b3031e2e2e56fc32f4da71`. The five process platforms and portable WASM each pass 91 canonical observations and saved rechecks, totaling 546 observations. All transferred receipts were independently checked locally against current source, helper, corpus, checker, lockfile, artifact and claim identities. +- Exact native `0.1.2` / CLI `0.1.1` pairing and compiler/extension `0.5.0` are qualified. Every process producer passes installed offline operation and full structured diagnostic parity with the Python API. Windows imports only allowed OS runtimes. The extracted process archive passes runtime switching, installed-byte integrity and unsupported-platform refusal. +- Final promotion stages 39 assets plus `SHA256SUMS`. Local audit verifies every final checksum, all 11 canonical process archive members, payload hashes, executable modes and all six raw MCK report/recheck pairs. A real four-platform input set is refused for missing Intel evidence. Historical WASM releases `0.4.0` and `0.4.2` remain available with their recorded digests. +- Source-pinned Rust host checks and ten fresh timing samples for small/medium workloads pass on the actual candidate bytes. Raw inputs, reports and toolchain/build identities are retained in the candidate and final host-evidence archive. No general performance claim is made. MoonBit routes remain unsupported at the documented pin with checked limitations. +- SDK `0.4.0` publication and the separate upstream Python JSON MCK profile are complete. The Windows checker build passes with short scoped Cargo storage and explicit parent/submodule Git settings; real source changes are still refused. No corpus change or integrity exception was accepted. +- Main specs are synced. All merges and admin bypass are approved. Python/native/extension release tags require exact maintainer approval once the merged source is concrete. Keep this change active until publication and final verification; archive afterward. Do not treat staged assets as published releases. diff --git a/openspec/specs/python/extension/spec.md b/openspec/specs/python/extension/spec.md index f8ca615e..1d288aeb 100644 --- a/openspec/specs/python/extension/spec.md +++ b/openspec/specs/python/extension/spec.md @@ -14,12 +14,16 @@ The migrated crate SHALL retain the `morphir-python-binding` package name and na - **THEN** it builds and invokes compile and generate using declared core/SDK dependencies without local source overrides or Python development libraries ### Requirement: MEP behavior remains compatible -The extension SHALL retain identity `morphir-python`, Python language/target identifiers, initialization and capability negotiation, compile/generate methods and advertised IR v3/v4 support. Native Rust, Python and packaged WASM invocations SHALL agree on supported model semantics and language diagnostic codes. (bead: morphir-ukvn.1.6) +The extension SHALL retain identity `morphir-python`, Python language/target identifiers, initialization and capability negotiation, legacy compile/generate methods and advertised IR v3/v4 support. Native Rust, Python, packaged WASM and process invocations SHALL agree on supported model semantics and language diagnostic codes. New model methods MUST require their negotiated draft and admitted capabilities. (bead: morphir-5hxy) #### Scenario: Existing host installs the replacement bundle - **WHEN** a compatible Morphir host installs the bundle built from morphir-python - **THEN** initialization, capability discovery, Python compilation and Python generation succeed through the existing protocol +#### Scenario: Users select different supported runtimes +- **WHEN** qualified WASM and process guests handle equivalent supported compilation or generation requests +- **THEN** they agree on compiler identity, supported semantics, deterministic artifacts and language diagnostic codes + ### Requirement: Python installation hides Rust implementation details Installing `morphir-python` on qualified CPython platforms SHALL install its compatible native compiler dependency automatically from wheels. Compilation and generation MUST require neither a Rust toolchain nor a separately installed Morphir CLI at runtime. The `morphir` distribution SHALL keep zero third-party runtime dependencies. (bead: morphir-ukvn.1.6) @@ -51,3 +55,40 @@ The cutover SHALL remove the Python binding implementation, workspace membership #### Scenario: Rust workspace after removal - **WHEN** the replacement provider has passed migration qualification and the coordinated removal lands - **THEN** the morphir-rust workspace and its CI remain valid with no Python compiler implementation or references requiring the former crate directory + +### Requirement: A native process guest serves framed MEP sessions +A released native process guest SHALL serve JSON-RPC MEP over Content-Length-framed stdin/stdout without CPython or Rust installed by the user. It MUST enforce documented header/body bounds, UTF-8 byte lengths, request identity and the SDK lifecycle. Stdout MUST contain only protocol frames; notifications MUST NOT receive responses. (bead: morphir-5hxy) + +#### Scenario: Multiple operations in one process +- **WHEN** a host initializes the guest, performs supported operations, requests shutdown and sends exit +- **THEN** responses retain request IDs, session capabilities remain fixed, and the process exits cleanly + +#### Scenario: Numeric and string request identity +- **WHEN** a supported request uses either a numeric or string JSON-RPC ID +- **THEN** its response preserves that exact ID type and value in both runtime routes + +#### Scenario: Malformed or oversized frame +- **WHEN** a frame has conflicting lengths, truncated data, invalid JSON, invalid UTF-8 or exceeds a declared bound +- **THEN** the guest refuses it without unbounded allocation or logging non-protocol text to stdout + +#### Scenario: Operation outside its lifecycle +- **WHEN** an operation is requested before initialization or after shutdown +- **THEN** the guest applies the protocol lifecycle refusal and does not invoke the compiler + +### Requirement: Both runtime choices have qualified release artifacts +Extension releases SHALL retain portable WASM and provide qualified process artifacts for each advertised process platform. Descriptors MUST bind runtime, platform, arguments, executable mode, digest, protocol versions and capability claims to the selected bytes. Unsupported runtime/platform combinations MUST fail explicitly. (bead: morphir-5hxy) + +#### Scenario: Process artifact is selected offline +- **WHEN** a compatible host selects a qualified installed process artifact +- **THEN** it verifies and launches the locked executable and arguments without obtaining Python, Rust or another artifact from the network + +#### Scenario: Existing user continues with WASM +- **WHEN** a compatible host selects the retained WASM runtime +- **THEN** the extension remains portable and its operations preserve the qualified compiler behavior + +### Requirement: Host support claims follow actual qualification +Each documented successful host route SHALL have retained candidate-bound installation, lifecycle and operation evidence for the stated host revision and runtime. Unqualified or incompatible Rust or MoonBit routes MUST be labeled unsupported with their observed limitations. A transport wrapper alone MUST NOT establish model-envelope support. (bead: morphir-5hxy) + +#### Scenario: A host requires unavailable model behavior +- **WHEN** a host route requires a profile or model operation that the selected guest cannot admit +- **THEN** documentation records that route as unsupported rather than claiming that process transport resolves the incompatibility diff --git a/openspec/specs/python/model-transport/spec.md b/openspec/specs/python/model-transport/spec.md new file mode 100644 index 00000000..6c770031 --- /dev/null +++ b/openspec/specs/python/model-transport/spec.md @@ -0,0 +1,58 @@ +# python/model-transport Specification + +## Purpose + +Expose the Python compiler through the current canonical MEP draft with explicit model transport negotiation, bounded envelopes and truthful supported-profile claims. + +## Requirements + +### Requirement: New model operations require exact draft negotiation +The extension SHALL support MEP `0.2.0-draft.1` for model operations while preserving legacy `0.1` behavior. Draft negotiation MUST match the full version exactly. A session negotiated as `0.1` MUST NOT enable model operations. Package, claims, model-envelope and IR versions MUST remain independent. (bead: morphir-5hxy) + +#### Scenario: Host offers the supported new draft +- **WHEN** a host and extension both offer `0.2.0-draft.1` +- **THEN** initialization selects the exact draft before any model operation is admitted + +#### Scenario: Legacy or different draft session +- **WHEN** the session selects `0.1` or the host offers only another draft version +- **THEN** model operations are unavailable and an incompatible-only handshake is refused + +### Requirement: Model operation admission is bilateral +Each model operation SHALL require an initialized, advertised tuple matching operation, profile, codec, envelope version and semantic IR version. Session tuples and limits MUST remain within both peers' offers and permitted static claims. Unknown required guarantees and unadmitted tuples MUST be refused. (bead: morphir-5hxy) + +#### Scenario: Compile support does not grant generation +- **WHEN** only a compile tuple was admitted and the host requests model generation +- **THEN** the operation is refused without invoking the generator + +#### Scenario: Independent lists would produce an unsupported combination +- **WHEN** an operation combines otherwise known tuple members in a combination not admitted as a complete tuple +- **THEN** the extension refuses that combination rather than forming a Cartesian product + +### Requirement: Model envelopes bind exact bounded bytes +Model payloads SHALL use the canonical `0.1.0-draft.1` envelope and validate codec, layout, canonical padded base64, decoded length, SHA-256, semantic IR version and required critical fields before compiler invocation. Advertised and local message/decoded limits MUST be enforced before unbounded allocation or decoding. (bead: morphir-5hxy) + +#### Scenario: Payload length or digest disagrees +- **WHEN** an envelope's declared length or digest differs from its decoded bytes +- **THEN** admission fails without compiling or generating + +#### Scenario: Oversized or unknown critical input +- **WHEN** input exceeds an applicable bound or names a critical field the receiver does not understand +- **THEN** the extension refuses the request and does not silently ignore that condition + +### Requirement: Initial profile support is explicit and lossless +The initial model adapter SHALL support `morphir-ir` with `morphir-json` for supported v3/v4 Library models through `compileModel` and `generateModel`. Unsupported rich Ion profiles, distribution forms or preservation guarantees MUST be refused explicitly. No automatic lossy conversion or fallback SHALL occur. (bead: morphir-5hxy) + +#### Scenario: Supported JSON model round trip +- **WHEN** a qualified JSON-profile model is compiled, generated and recompiled using an admitted tuple +- **THEN** its supported semantics and diagnostics agree with the existing compiler for the selected IR version + +#### Scenario: Rich model requires unsupported preservation +- **WHEN** a host requests an Ion profile or guarantee not advertised by this increment +- **THEN** the extension refuses it rather than converting to JSON and discarding information + +### Requirement: New protocol claims require executable conformance evidence +The extension SHALL advertise the new draft and model tuples only after candidate-bound executable guest qualification covers negotiation, lifecycle, supported operations and negative admission cases. Documentation MUST distinguish implemented profiles from unimplemented optional MEP capabilities. (bead: morphir-5hxy) + +#### Scenario: Protocol constants exist without operation support +- **WHEN** an SDK exposes model types or method names but the Python adapter has not passed qualification +- **THEN** those symbols do not establish or justify an advertised support claim diff --git a/packages/morphir-tools/README.md b/packages/morphir-tools/README.md index 2fa1c872..5ca42248 100644 --- a/packages/morphir-tools/README.md +++ b/packages/morphir-tools/README.md @@ -75,7 +75,7 @@ format-version rules and the draft v4 status. ## Requirements - Python 3.14+ -- `morphir>=0.1.0` and `morphir-python-native==0.1.1` (installed as dependencies) +- `morphir>=0.1.0` and `morphir-python-native==0.1.2` (installed as dependencies) ## License diff --git a/packages/morphir-tools/pyproject.toml b/packages/morphir-tools/pyproject.toml index f8c7de40..b879f22e 100644 --- a/packages/morphir-tools/pyproject.toml +++ b/packages/morphir-tools/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "morphir-python" -version = "0.1.0" +version = "0.1.1" description = "CLI tools and extensions for Morphir Python" readme = "README.md" license = "Apache-2.0" @@ -24,7 +24,7 @@ authors = [ dependencies = [ "morphir>=0.1.0", # Exact qualified native compiler; checked again when the compiler first loads. - "morphir-python-native==0.1.1", + "morphir-python-native==0.1.2", "typer>=0.15", # Rich uses Pygments; older versions are affected by CVE-2026-4539. "pygments>=2.20.0", diff --git a/packages/morphir-tools/src/morphir_tools/__init__.py b/packages/morphir-tools/src/morphir_tools/__init__.py index 22222dd3..5b33636e 100644 --- a/packages/morphir-tools/src/morphir_tools/__init__.py +++ b/packages/morphir-tools/src/morphir_tools/__init__.py @@ -9,5 +9,5 @@ '0.1.0' """ -__version__ = "0.1.0" +__version__ = "0.1.1" __all__ = ["__version__"] diff --git a/packages/morphir-tools/src/morphir_tools/python/_bridge.py b/packages/morphir-tools/src/morphir_tools/python/_bridge.py index e23a4bd1..5a364ea8 100644 --- a/packages/morphir-tools/src/morphir_tools/python/_bridge.py +++ b/packages/morphir-tools/src/morphir_tools/python/_bridge.py @@ -8,7 +8,7 @@ from ._types import NativeCompilerInfo, NativeCompilerUnavailable -EXPECTED_NATIVE_VERSION = "0.1.1" +EXPECTED_NATIVE_VERSION = "0.1.2" """The exact ``morphir-python-native`` version this release was qualified with. It must equal the pin in this distribution's ``pyproject.toml``. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 450204fc..fbaeac02 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1430,9 +1430,9 @@ dependencies = [ [[package]] name = "morphir-extension-sdk" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcb8913b6cb5d45fc20aadd638ddb0c1d83efac9868b8409937e4cfca6babc0" +checksum = "56820fd793c213eb0cd571cc6af6e24196171dbb40e4ab5f65c3838ca6c73dd8" dependencies = [ "base64 0.22.1", "extism-pdk", @@ -1460,11 +1460,12 @@ dependencies = [ [[package]] name = "morphir-python-binding" -version = "0.4.2" +version = "0.5.0" dependencies = [ "cucumber", "morphir-core", "morphir-extension-sdk", + "morphir-json", "num-bigint 0.5.1", "ruff_python_ast", "ruff_python_codegen", @@ -1475,9 +1476,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "morphir-python-extension" +version = "0.1.0" +dependencies = [ + "morphir-extension-sdk", + "morphir-python-binding", + "serde", + "serde_json", +] + [[package]] name = "morphir-python-native" -version = "0.1.1" +version = "0.1.2" dependencies = [ "morphir-extension-sdk", "morphir-python-binding", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 40bfa856..c729b844 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/morphir-python-binding", "crates/morphir-python-native"] +members = ["crates/morphir-python-binding", "crates/morphir-python-native", "crates/morphir-python-extension"] resolver = "2" [workspace.package] @@ -13,7 +13,8 @@ homepage = "https://github.com/finos/morphir-python" [workspace.dependencies] # Published Morphir crates. Never use sibling-checkout paths here. morphir-core = "=0.2.0" -morphir-extension-sdk = "=0.3.0" +morphir-json = "=0.2.0" +morphir-extension-sdk = "=0.4.0" num-bigint = "0.5" # Ruff component crates evolve together; update this group as one unit. ruff_python_parser = "=0.0.15" @@ -21,6 +22,7 @@ ruff_python_ast = "=0.0.15" ruff_python_codegen = "=0.0.15" ruff_text_size = "=0.0.15" serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } tempfile = "3" [profile.release] diff --git a/rust/crates/morphir-python-binding/Cargo.toml b/rust/crates/morphir-python-binding/Cargo.toml index 81ff6904..1be1816c 100644 --- a/rust/crates/morphir-python-binding/Cargo.toml +++ b/rust/crates/morphir-python-binding/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-binding" -version = "0.4.2" +version = "0.5.0" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -15,6 +15,7 @@ crate-type = ["rlib", "cdylib"] [dependencies] num-bigint.workspace = true morphir-core.workspace = true +morphir-json.workspace = true morphir-extension-sdk.workspace = true ruff_python_parser.workspace = true ruff_python_ast.workspace = true diff --git a/rust/crates/morphir-python-binding/README.md b/rust/crates/morphir-python-binding/README.md index 1b67cebc..ad11b4ec 100644 --- a/rust/crates/morphir-python-binding/README.md +++ b/rust/crates/morphir-python-binding/README.md @@ -11,7 +11,8 @@ This is an initial implementation of a defined subset of Python 3.12 and later and Morphir IR v3 and v4. It supports ADT declarations, fixed tuples and annotated pure conditional functions, typed calls and unary lambdas. It does not implement all Python syntax or all Morphir IR nodes, and does not claim full Morphir Compatibility Kit conformance for this extension. -The crate depends on the published `morphir-core` and `morphir-extension-sdk` crates only. +Morphir dependencies use published `morphir-core`, `morphir-json` and +`morphir-extension-sdk` crates. It does not link CPython; the separate `morphir-python-native` crate in this workspace adapts it for the `morphir-python` Python package. @@ -23,6 +24,32 @@ and releases it. Bundles up to `extension/python/v0.4.0` remain available from t function support, and Morphir CLI `0.4.0-beta.1` and later cannot compile with it. Use the [extension guide](../../../docs/python-extension.md) to build and install the bundle. +## Development model adapter + +The direct Rust `Frontend::compile_model` and `Backend::generate_model` methods +share the legacy compiler. Model transport is not yet advertised in MEP discovery; +protocol qualification and the published SDK prerequisite remain in progress in +`add-process-mep-extension`. + +| Boundary | Initial support | +| --- | --- | +| Operation | `compileModel`, `generateModel` | +| Profile and codec | `morphir-ir`, `morphir-json` | +| Envelope version | `0.1.0-draft.1` | +| Semantic IR versions | `3.0.0`, draft `4.0.0` | +| Distribution | `Library`, within the documented Python subset | +| Evidence | JSON only; no preservation or typing guarantees established | +| Encoded / decoded bytes | 12 MiB / 8 MiB, aggregated with dependencies and evidence | +| Parse budget | 128 levels, 100,000 aggregate value nodes | + +Admission checks canonical padded base64, exact decoded length and SHA-256, +semantic IR version, critical fields and resource bounds before generation. +Compilation output has the same decoded-byte bound. Model compilation preserves +legacy diagnostics and refuses dependency compilation. Generation refuses `Specs` +and `Application` distributions with `PY004`. Ion payloads and evidence, other +profiles, unsupported tuples and required guarantees are refused without +conversion. Limits negotiated by a session can only narrow these static bounds. + ## A complete model Save this as `models.py`. It is valid Python and supported frontend input: @@ -555,7 +582,7 @@ workspace. Use the same published SDK version as this crate: ```toml [dependencies] morphir-python-binding = { git = "https://github.com/finos/morphir-python", rev = "" } -morphir-extension-sdk = "=0.3.0" +morphir-extension-sdk = "=0.4.0" serde_json = "1" ``` diff --git a/rust/crates/morphir-python-binding/src/lib.rs b/rust/crates/morphir-python-binding/src/lib.rs index 77c08d98..bc55b7cb 100644 --- a/rust/crates/morphir-python-binding/src/lib.rs +++ b/rust/crates/morphir-python-binding/src/lib.rs @@ -26,6 +26,7 @@ mod backend; mod frontend; mod ir; +mod model_transport; mod modules; mod names; mod values; @@ -66,12 +67,20 @@ impl Extension for PythonExtension { ir_versions: vec!["3".into(), "4".into()], generate: true, }), + model_transport: Some(model_transport::support()), ..Default::default() } } } impl Frontend for PythonExtension { + fn compile_model( + &self, + request: morphir_extension_sdk::model::CompileModelRequest, + ) -> Result { + model_transport::compile(self, request) + } + fn compile(&self, request: CompileRequest) -> Result { let diagnostics = if frontend::parse_stage_requested(&request) { vec![Diagnostic { @@ -118,6 +127,13 @@ impl Frontend for PythonExtension { } impl Backend for PythonExtension { + fn generate_model( + &self, + request: morphir_extension_sdk::model::GenerateModelRequest, + ) -> Result { + model_transport::generate(self, request) + } + fn generate(&self, request: GenerateRequest) -> Result { Ok(match backend::generate(&request) { Ok(artifacts) => GenerateResult { diff --git a/rust/crates/morphir-python-binding/src/model_transport.rs b/rust/crates/morphir-python-binding/src/model_transport.rs new file mode 100644 index 00000000..0243b950 --- /dev/null +++ b/rust/crates/morphir-python-binding/src/model_transport.rs @@ -0,0 +1,215 @@ +//! Explicit JSON-model admission around the shared compiler, without transcoding. +use crate::{PythonExtension, error}; +use morphir_extension_sdk::model::*; +use morphir_extension_sdk::prelude::*; + +pub(crate) fn support() -> ModelTransportSupport { + ModelTransportSupport { + tuples: [ModelOperation::CompileModel, ModelOperation::GenerateModel] + .into_iter() + .flat_map(|operation| { + ["3.0.0", "4.0.0"].map(|version| ModelTransportTuple { + operation, + profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, + envelope_version: MODEL_ENVELOPE_VERSION.into(), + ir_version: version.into(), + }) + }) + .collect(), + // Base64 payloads leave at least 4 MiB for envelope/source metadata in + // the process transport's 16 MiB complete-message bound. + limits: ModelTransportLimits { + max_encoded_bytes: 12 * 1024 * 1024, + max_decoded_bytes: 8 * 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + guarantees: vec![], + } +} + +fn invalid(error: impl std::fmt::Display) -> ExtensionError { + ExtensionError::InvalidParams(format!("MEP model admission: {error}")) +} + +fn admit(method: &str, value: &serde_json::Value) -> Result<()> { + let support = support(); + let admitted = NegotiatedModelTransport::select(&support, &support).map_err(invalid)?; + let mut capabilities = PythonExtension::capabilities(); + capabilities.model_transport = Some(support); + validate_model_invocation(&admitted, &capabilities, method, value).map_err(invalid) +} + +// Native SDK builds can inspect Ion evidence, but WASM cannot. Keep this +// adapter's accepted subset identical across runtimes, including sidecars. +fn require_json(model: &MorphirModel) -> Result<()> { + if model.payload.codec != ModelCodec::MorphirJson + || model + .evidence + .iter() + .any(|item| item.evidence.codec != ModelCodec::MorphirJson) + { + return Err(invalid( + "Python model transport is JSON-only, including evidence", + )); + } + Ok(()) +} + +pub(crate) fn compile( + extension: &PythonExtension, + request: CompileModelRequest, +) -> Result { + for dependency in &request.dependencies { + require_json(&dependency.model)?; + } + admit( + "morphir.frontend.compileModel", + &serde_json::to_value(&request)?, + )?; + if !request.dependencies.is_empty() { + return Ok(CompileModelResult::Failed { + diagnostics: vec![error("PY001", "Expected Python and no dependencies")], + }); + } + let model_id = request.package.name.clone(); + let result = extension.compile(CompileRequest { + language_id: request.language_id, + sources: request.sources, + package: request.package, + dependencies: vec![], + options: request.options, + baseline: None, + })?; + if !result.success { + return Ok(CompileModelResult::Failed { + diagnostics: result.diagnostics, + }); + } + let ir = result + .ir + .ok_or_else(|| invalid("successful compiler result is missing IR"))?; + let bytes = bounded_json(&ir, support().limits.max_decoded_bytes)?; + let payload = ModelPayload::from_bytes(ModelCodec::MorphirJson, &bytes).map_err(invalid)?; + let model = MorphirModel { + envelope_version: MODEL_ENVELOPE_VERSION.into(), + model_id, + revision: format!("sha256:{}", payload.sha256), + ir_version: request.output.ir_version.clone(), + profile: request.output.profile.clone(), + payload, + evidence: vec![], + critical: vec![], + }; + retain_model(&model, &request.output, &support().limits).map_err(invalid)?; + Ok(CompileModelResult::Compiled { + model: Box::new(model), + diagnostics: result.diagnostics, + modules: result.modules, + }) +} + +pub(crate) fn generate( + extension: &PythonExtension, + request: GenerateModelRequest, +) -> Result { + require_json(&request.model)?; + admit( + "morphir.backend.generateModel", + &serde_json::to_value(&request)?, + )?; + let retained = + retain_model(&request.model, &request.input, &support().limits).map_err(invalid)?; + let limits = support().limits; + let source = std::str::from_utf8(retained.bytes()).map_err(invalid)?; + let ir = morphir_json::read( + source, + &mut JsonBudget { + remaining: limits.max_nodes, + depth: limits.max_depth as usize, + string_bytes: limits.max_decoded_bytes as usize, + }, + ) + .map_err(invalid)?; + extension.generate(GenerateRequest { + ir, + target: request.target, + options: request.options, + }) +} + +/// Stop JSON serialization before extending the payload buffer beyond its bound. +fn bounded_json(value: &serde_json::Value, limit: u32) -> Result> { + struct Buffer { + bytes: Vec, + limit: usize, + } + impl std::io::Write for Buffer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + if self.bytes.len().saturating_add(bytes.len()) > self.limit { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "model payload exceeds decoded limit", + )); + } + self.bytes.extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + let mut buffer = Buffer { + bytes: Vec::new(), + limit: limit as usize, + }; + serde_json::to_writer(&mut buffer, value).map_err(invalid)?; + Ok(buffer.bytes) +} + +// Materialize only after SDK admission, using the published literal-JSON reader +// so serde-private-looking keys cannot acquire a different scalar meaning. +struct JsonBudget { + remaining: u32, + depth: usize, + string_bytes: usize, +} +impl morphir_json::ReadBudget for JsonBudget { + type Error = &'static str; + fn before_value( + &mut self, + depth: usize, + _: morphir_json::ValueKind, + ) -> std::result::Result<(), Self::Error> { + if depth >= self.depth { + return Err("model depth limit"); + } + self.remaining = self.remaining.checked_sub(1).ok_or("model node limit")?; + Ok(()) + } + fn before_string(&mut self, value: &str) -> std::result::Result<(), Self::Error> { + if value.len() > self.string_bytes { + Err("model string limit") + } else { + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn output_json_stops_at_its_decoded_budget() { + let value = serde_json::json!("λ"); + assert_eq!(bounded_json(&value, 4).unwrap(), "\"λ\"".as_bytes()); + assert!( + bounded_json(&value, 3) + .unwrap_err() + .to_string() + .contains("decoded limit") + ); + } +} diff --git a/rust/crates/morphir-python-binding/tests/model_session.rs b/rust/crates/morphir-python-binding/tests/model_session.rs new file mode 100644 index 00000000..f9a37c79 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/model_session.rs @@ -0,0 +1,89 @@ +//! Real SDK sessions must admit only the negotiated model tuples and limits. +use morphir_extension_sdk::{NativeExtension, NativeProtocol}; +use morphir_python_binding::PythonExtension; +use serde_json::{Value, json}; + +fn send(protocol: &dyn NativeProtocol, request: Value) -> Value { + serde_json::to_value(protocol.handle(serde_json::from_value(request).unwrap())).unwrap() +} + +fn host_offer() -> Value { + json!({ + "tuples":[{"operation":"morphir.frontend.compileModel","profile":"morphir-ir", + "codec":"morphir-json","envelopeVersion":"0.1.0-draft.1","irVersion":"3.0.0"}], + "limits":{"maxEncodedBytes":65536,"maxDecodedBytes":49152,"maxDepth":64,"maxNodes":10000}, + "guarantees":[], + }) +} + +fn initialize(versions: Value, offer: Option) -> Value { + let mut params = + json!({"protocolVersions":versions,"host":{"name":"qualified-test","version":"1"}}); + if let Some(offer) = offer { + params["modelTransport"] = offer; + } + json!({"jsonrpc":"2.0","id":1,"method":"morphir.initialize","params":params}) +} + +fn compile_model(version: &str) -> Value { + json!({"jsonrpc":"2.0","id":2,"method":"morphir.frontend.compileModel","params":{ + "languageId":"python","sources":{"documents":[{"uri":"main.py","languageId":"python","version":1,"text":"def identity(flag: bool) -> bool:\n return flag\n"}]}, + "package":{"name":"mck/python"},"dependencies":[], + "options":{"irVersion":version,"typesOnly":false},"contextDigest":"a".repeat(64), + "output":{"operation":"morphir.frontend.compileModel","profile":"morphir-ir","codec":"morphir-json","envelopeVersion":"0.1.0-draft.1","irVersion":version}, + }}) +} + +#[test] +fn draft_session_selects_complete_tuples_and_each_lower_limit() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let session = extension.open_protocol(); + let selected = send( + session.as_ref(), + initialize(json!(["0.2.0-draft.1", "0.1"]), Some(host_offer())), + ); + assert!(selected.get("error").is_none(), "{selected}"); + assert_eq!(selected["result"]["protocolVersion"], "0.2.0-draft.1"); + assert_eq!( + selected["result"]["capabilities"]["modelTransport"], + host_offer() + ); + let compiled = send(session.as_ref(), compile_model("3.0.0")); + assert!(compiled.get("error").is_none(), "{compiled}"); + assert_eq!(compiled["result"]["model"]["irVersion"], "3.0.0"); + let refused = send(session.as_ref(), compile_model("4.0.0")); + assert!( + refused.get("error").is_some(), + "unoffered tuple admitted: {refused}" + ); + let again = send(session.as_ref(), initialize(json!(["0.1"]), None)); + assert!(again.get("error").is_some(), "session was renegotiated"); + let still_refused = send(session.as_ref(), compile_model("4.0.0")); + assert!(still_refused.get("error").is_some()); +} + +#[test] +fn legacy_or_other_drafts_cannot_admit_model_operations() { + let extension = NativeExtension::frontend_backend(PythonExtension).unwrap(); + let legacy = extension.open_protocol(); + let selected = send( + legacy.as_ref(), + initialize(json!(["0.1"]), Some(host_offer())), + ); + assert!(selected.get("error").is_none(), "{selected}"); + assert_eq!(selected["result"]["protocolVersion"], "0.1"); + assert!(selected["result"]["capabilities"]["modelTransport"].is_null()); + assert!( + send(legacy.as_ref(), compile_model("3.0.0")) + .get("error") + .is_some() + ); + for (versions, offer) in [ + (json!(["0.2.0-draft.2"]), Some(host_offer())), + (json!(["0.2.0-draft.1"]), None), + ] { + let session = extension.open_protocol(); + let refused = send(session.as_ref(), initialize(versions, offer)); + assert_eq!(refused["error"]["code"], -32011); + } +} diff --git a/rust/crates/morphir-python-binding/tests/model_transport.rs b/rust/crates/morphir-python-binding/tests/model_transport.rs new file mode 100644 index 00000000..9ee74cb4 --- /dev/null +++ b/rust/crates/morphir-python-binding/tests/model_transport.rs @@ -0,0 +1,292 @@ +//! The model boundary retains supported semantics and refuses unsupported admission. +use morphir_extension_sdk::model::*; +use morphir_extension_sdk::prelude::*; +use morphir_python_binding::PythonExtension; +use serde_json::{Value, json}; + +fn a_request(version: &str) -> CompileModelRequest { + CompileModelRequest { + language_id: "python".into(), + sources: SourceSet { + root: None, + documents: vec![SourceDocument { + uri: "model.py".into(), language_id: "python".into(), version: 1, + text: "from dataclasses import dataclass\n@dataclass(frozen=True)\nclass Box:\n value: int\n".into(), + }], + }, + package: CompilePackage { name: "acme".into(), exposed_modules: None }, + dependencies: vec![], + options: CompileOptions { ir_version: version.into(), ..Default::default() }, + context_digest: "a".repeat(64), + output: ModelTransportTuple { + operation: ModelOperation::CompileModel, profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, envelope_version: MODEL_ENVELOPE_VERSION.into(), ir_version: version.into(), + }, + } +} + +fn a_model(version: &str) -> MorphirModel { + match PythonExtension.compile_model(a_request(version)).unwrap() { + CompileModelResult::Compiled { model, .. } => *model, + CompileModelResult::Failed { diagnostics } => panic!("{diagnostics:?}"), + } +} + +fn generation(model: MorphirModel) -> GenerateModelRequest { + GenerateModelRequest { + input: ModelTransportTuple { + operation: ModelOperation::GenerateModel, + profile: "morphir-ir".into(), + codec: ModelCodec::MorphirJson, + envelope_version: MODEL_ENVELOPE_VERSION.into(), + ir_version: model.ir_version.clone(), + }, + model, + target: "python".into(), + context_digest: "a".repeat(64), + required_guarantees: vec![], + options: Default::default(), + } +} + +#[test] +fn json_models_compile_and_generate_with_the_legacy_semantics() { + for version in ["3.0.0", "4.0.0"] { + let model = a_model(version); + assert_eq!(model.envelope_version, MODEL_ENVELOPE_VERSION); + assert_eq!(model.ir_version, version); + assert_eq!(model.model_id, "acme"); + let request = generation(model); + let retained = retain_model( + &request.model, + &request.input, + &ModelTransportLimits { + max_encoded_bytes: 1024 * 1024, + max_decoded_bytes: 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + ) + .unwrap(); + let ir: Value = serde_json::from_slice(retained.bytes()).unwrap(); + let legacy = PythonExtension + .generate(GenerateRequest { + ir, + target: "python".into(), + options: Default::default(), + }) + .unwrap(); + let generated = PythonExtension.generate_model(request).unwrap(); + assert!(generated.success, "{:?}", generated.diagnostics); + let mut recompile = a_request(version); + recompile.sources.documents = generated + .artifacts + .iter() + .map(|artifact| SourceDocument { + uri: artifact.path.clone(), + language_id: "python".into(), + version: 1, + text: artifact.content.clone(), + }) + .collect(); + let reparsed = PythonExtension.compile_model(recompile).unwrap(); + let CompileModelResult::Compiled { + model: reparsed, .. + } = reparsed + else { + panic!("generated model should recompile") + }; + assert_eq!(reparsed.payload, a_model(version).payload); + assert_eq!( + serde_json::to_value(generated).unwrap(), + serde_json::to_value(legacy).unwrap() + ); + } +} + +#[test] +fn model_compilation_preserves_language_diagnostics() { + let mut request = a_request("4.0.0"); + request.sources.documents[0].text = "import os\n".into(); + let legacy = PythonExtension + .compile(CompileRequest { + language_id: request.language_id.clone(), + sources: request.sources.clone(), + package: request.package.clone(), + dependencies: vec![], + options: request.options.clone(), + baseline: None, + }) + .unwrap(); + assert!(!legacy.success); + let CompileModelResult::Failed { diagnostics } = + PythonExtension.compile_model(request).unwrap() + else { + panic!("unsupported source must fail") + }; + assert_eq!( + serde_json::to_value(diagnostics).unwrap(), + serde_json::to_value(legacy.diagnostics).unwrap() + ); +} + +#[test] +fn malformed_envelopes_guarantees_and_unadvertised_forms_are_refused() { + let baseline = generation(a_model("4.0.0")); + let mut value = serde_json::to_value(&baseline).unwrap(); + value["model"]["critical"] = json!([]); + for (path, invalid) in [ + ("/model/payload/sha256", json!("0".repeat(64))), + ("/model/payload/decodedByteLength", json!(1)), + ("/model/payload/content", json!("not canonical base64")), + ("/model/critical", json!(["future.required"])), + ("/model/irVersion", json!("3.0.0")), + ("/input/codec", json!("ion-binary")), + ("/input/profile", json!("rich-model")), + ("/input/operation", json!("morphir.frontend.compileModel")), + ( + "/requiredGuarantees", + json!([{"guarantee":"example.typed","scope":"model:acme"}]), + ), + ] { + let mut invalid_request = value.clone(); + *invalid_request.pointer_mut(path).unwrap() = invalid; + let request = serde_json::from_value(invalid_request).unwrap(); + assert!( + PythonExtension.generate_model(request).is_err(), + "accepted {path}" + ); + } + let mut request = a_request("4.0.0"); + request.output.ir_version = "3.1.0".into(); + assert!(PythonExtension.compile_model(request).is_err()); +} + +#[test] +fn model_round_trip_retains_large_integer_precision_and_v3_refuses_it() { + let decimal = "1234567890123456789012345678901234567890"; + for version in ["3.0.0", "4.0.0"] { + let mut request = a_request(version); + request.options.types_only = false; + request.sources.documents[0].text = format!("def number() -> int:\n return {decimal}\n"); + match PythonExtension.compile_model(request).unwrap() { + CompileModelResult::Compiled { model, .. } => { + assert_eq!(version, "4.0.0"); + let generated = PythonExtension.generate_model(generation(*model)).unwrap(); + assert!(generated.success, "{:?}", generated.diagnostics); + assert!( + generated + .artifacts + .iter() + .any(|artifact| artifact.content.contains(decimal)) + ); + } + CompileModelResult::Failed { diagnostics } => { + assert_eq!(version, "3.0.0"); + assert!( + diagnostics + .iter() + .any(|item| item.message.contains("64-bit")) + ); + } + } + } +} + +#[test] +fn aggregate_nodes_and_dependency_support_are_bounded() { + let mut request = generation(a_model("4.0.0")); + let retained = retain_model( + &request.model, + &request.input, + &ModelTransportLimits { + max_encoded_bytes: 1024 * 1024, + max_decoded_bytes: 1024 * 1024, + max_depth: 128, + max_nodes: 100_000, + }, + ) + .unwrap(); + let mut ir: Value = serde_json::from_slice(retained.bytes()).unwrap(); + ir["extra"] = json!(vec![0; 100_001]); + request.model.payload = + ModelPayload::from_bytes(ModelCodec::MorphirJson, &serde_json::to_vec(&ir).unwrap()) + .unwrap(); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("node_limit"), "{error}"); + + let mut request = a_request("4.0.0"); + request.dependencies.push(CompileModelDependency { + package_name: "dep".into(), + model: a_model("4.0.0"), + }); + let CompileModelResult::Failed { diagnostics } = + PythonExtension.compile_model(request).unwrap() + else { + panic!("dependencies are unsupported") + }; + assert_eq!(diagnostics[0].code.as_deref(), Some("PY001")); +} + +#[test] +fn ion_evidence_is_refused_uniformly_across_native_and_wasm() { + let mut request = generation(a_model("4.0.0")); + request.model.evidence.push(ModelGuaranteeEvidence { + guarantee: "urn:example:optional".into(), + model_id: request.model.model_id.clone(), + model_revision: request.model.revision.clone(), + context_digest: request.context_digest.clone(), + scope: format!("model:{}", request.model.model_id), + producer: "fixture".into(), + evidence: ModelPayload::from_bytes(ModelCodec::IonBinary, &[0xe0, 1, 0, 0xea, 0x21, 1]) + .unwrap(), + }); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("JSON-only"), "{error}"); +} + +#[test] +fn unsupported_distribution_forms_are_explicit_language_refusals() { + for distribution in [ + json!({"Specs":{"packageName":"acme","dependencies":{},"spec":{"modules":{}}}}), + json!({"Application":{"packageName":"acme","dependencies":{},"def":{"modules":{}},"entryPoints":{}}}), + ] { + let ir = json!({"formatVersion":4,"distribution":distribution}); + // Prove these are complete valid IR, rather than malformed enum inputs. + serde_json::from_value::(ir.clone()).unwrap(); + let mut request = generation(a_model("4.0.0")); + request.model.payload = + ModelPayload::from_bytes(ModelCodec::MorphirJson, &serde_json::to_vec(&ir).unwrap()) + .unwrap(); + let result = PythonExtension.generate_model(request).unwrap(); + assert!(!result.success); + assert!(result.artifacts.is_empty()); + assert_eq!(result.diagnostics[0].code.as_deref(), Some("PY004")); + assert!(result.diagnostics[0].message.contains("Only Library")); + } +} + +#[test] +fn semantic_versions_and_payload_resource_limits_are_checked_before_generation() { + let payloads = [ + ( + br#"{"formatVersion":3,"distribution":{}}"#.to_vec(), + "unsupported_ir_version", + ), + ( + format!("{}0{}", "[".repeat(128), "]".repeat(128)).into_bytes(), + "depth_limit", + ), + (vec![b' '; 8 * 1024 * 1024 + 1], "decoded_limit"), + ]; + for (bytes, reason) in payloads { + let mut request = generation(a_model("4.0.0")); + request.model.payload = ModelPayload::from_bytes(ModelCodec::MorphirJson, &bytes).unwrap(); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains(reason), "{reason}: {error}"); + } + let mut request = generation(a_model("4.0.0")); + request.model.payload.content = "A".repeat(12 * 1024 * 1024 + 4); + let error = PythonExtension.generate_model(request).unwrap_err(); + assert!(error.to_string().contains("encoded_limit"), "{error}"); +} diff --git a/rust/crates/morphir-python-binding/tests/support/native.rs b/rust/crates/morphir-python-binding/tests/support/native.rs index cdcd69ee..0d86af27 100644 --- a/rust/crates/morphir-python-binding/tests/support/native.rs +++ b/rust/crates/morphir-python-binding/tests/support/native.rs @@ -25,7 +25,12 @@ pub fn an_initialized_extension() -> NativeExtension { .unwrap(), ); assert!(response.error.is_none(), "{:?}", response.error); - let expected = serde_json::to_value(PythonExtension::capabilities()).unwrap(); + let mut capabilities = PythonExtension::capabilities(); + assert!(capabilities.model_transport.is_some()); + // This fixture deliberately selects legacy0.1; model support is static + // inventory, but must be absent from the selected legacy session. + capabilities.model_transport = None; + let expected = serde_json::to_value(capabilities).unwrap(); assert_eq!(expected["frontend"]["multiDocument"], true); assert_eq!(response.result.unwrap()["capabilities"], expected); let response = extension diff --git a/rust/crates/morphir-python-extension/Cargo.toml b/rust/crates/morphir-python-extension/Cargo.toml new file mode 100644 index 00000000..9c806c4c --- /dev/null +++ b/rust/crates/morphir-python-extension/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "morphir-python-extension" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +description = "Native stdio MEP guest for the Morphir Python compiler" +publish = false + +[dependencies] +morphir-python-binding = { path = "../morphir-python-binding", version = "0.5.0" } +morphir-extension-sdk.workspace = true +serde.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } diff --git a/rust/crates/morphir-python-extension/README.md b/rust/crates/morphir-python-extension/README.md new file mode 100644 index 00000000..0d1f220e --- /dev/null +++ b/rust/crates/morphir-python-extension/README.md @@ -0,0 +1,59 @@ +# Morphir Python process guest + +This crate runs the shared Python compiler as a native MEP process. It has no +CPython or PyO3 dependency, and the executable needs no user Rust toolchain. +Source is parsed statically and is never imported or executed. + +Development build and entry point: + +```bash +cargo build --manifest-path rust/Cargo.toml --release -p morphir-python-extension +./rust/target/release/morphir-python-extension --stdio +``` + +With no arguments the executable also serves stdio, matching the canonical +native bundler and installed process launcher. `--stdio` is an explicit alias; +`--help` prints usage and exits. + +In stdio mode stdout contains only Content-Length-framed UTF-8 JSON-RPC. +Headers are bounded to 8 KiB and complete bodies to 16 MiB. Lengths count bytes; +unknown headers are ignored and duplicate lengths are refused. A damaged frame +ends the connection. Invalid JSON in an otherwise valid frame gets a parse-error +response and the next framed request can recover. Notifications get no response; +`morphir.exit` ends the process. Each process owns its SDK protocol session. + +The host owns deadlines and process cleanup. A native process has ordinary OS +rights; choosing it does not provide a WASM sandbox. + +The current development build advertises legacy MEP 0.1. Exact-draft discovery +and model transport wait for the qualified published SDK prerequisite and +conformance checks. Platform archives and installation recipes are still under +development. The retained WASM release remains the qualified installation route. + +## Stage a development bundle + +From a clean candidate checkout, build the release executable and run the pinned +Morphir CLI's native bundler through the packaging helper: + +```bash +python -m scripts.process_extension \ + --executable rust/target/release/morphir-python-extension \ + --output .dev/process-bundle \ + --commit "$(git rev-parse HEAD)" \ + --report .dev/qualification/process-package.json \ + --host-check +``` + +On Windows the executable path ends in `.exe`. The helper selects an explicit +supported target, verifies the pinned CLI archive and stages a versioned raw +executable, its checksum and canonical `release.json`. It records the canonical +default argument list, explicit `--stdio` alias, executable mode, source commit, +Cargo lock digest, bundler identity, exact artifact bytes and live claims in the +separate report. Receipts are not additional members of the canonical bundle. + +`--host-check` requires the development Python environment with the matching +native compiler. It publishes into a temporary local repository, installs into +an isolated Morphir home, deletes the repository and runs offline v3/v4 +compile/generate/recompile comparisons against the Python API. This legacy route +check does not qualify the new MEP draft. Platform release builds, new-draft +qualification and promotion gates remain in progress. diff --git a/rust/crates/morphir-python-extension/src/framing.rs b/rust/crates/morphir-python-extension/src/framing.rs new file mode 100644 index 00000000..916cfdb3 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/framing.rs @@ -0,0 +1,100 @@ +//! Bounded Content-Length framing for UTF-8 MEP messages. +//! +//! Framing preserves message bytes. The session layer validates JSON and MEP. +//! +//! ``` +//! use morphir_python_extension::framing::{read_frame, write_frame}; +//! let mut wire = Vec::new(); +//! write_frame(&mut wire, br#"{"jsonrpc":"2.0","id":1}"#).unwrap(); +//! assert!(read_frame(&mut std::io::Cursor::new(wire)).unwrap().is_some()); +//! ``` +use std::io::{self, BufRead, Write}; + +/// Maximum header size, including the terminating CRLF pair. +pub const MAX_HEADER_BYTES: usize = 8 * 1024; +/// Maximum complete UTF-8 JSON body, including model-envelope overhead. +pub const MAX_BODY_BYTES: usize = 16 * 1024 * 1024; + +fn invalid(message: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +/// Read one bounded frame, or return `None` for EOF between messages. +/// +/// Duplicate or invalid lengths, non-ASCII headers, truncation and invalid +/// UTF-8 bodies are refused. Body limits are checked before allocation. +pub fn read_frame(reader: &mut impl BufRead) -> io::Result>> { + let mut header = Vec::with_capacity(128); + loop { + let mut byte = [0]; + match reader.read_exact(&mut byte) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::UnexpectedEof && header.is_empty() => { + return Ok(None); + } + Err(error) => return Err(error), + } + if header.len() == MAX_HEADER_BYTES { + return Err(invalid("MEP header exceeds 8 KiB")); + } + header.push(byte[0]); + if header.ends_with(b"\r\n\r\n") { + break; + } + } + if !header.is_ascii() { + return Err(invalid("MEP headers must be ASCII")); + } + let text = std::str::from_utf8(&header).map_err(|_| invalid("Invalid MEP header"))?; + let mut length = None; + for line in text[..text.len() - 4].split("\r\n") { + if line + .bytes() + .any(|byte| byte.is_ascii_control() && byte != b'\t') + { + return Err(invalid("Invalid control character in MEP header")); + } + let (name, value) = line + .split_once(':') + .ok_or_else(|| invalid("Malformed MEP header"))?; + if name.is_empty() + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte)) + { + return Err(invalid("Invalid MEP header name")); + } + if name.eq_ignore_ascii_case("Content-Length") { + if length.is_some() { + return Err(invalid("Duplicate Content-Length")); + } + let value = value.trim(); + if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(invalid("Content-Length must contain decimal digits")); + } + let size = value + .parse::() + .map_err(|_| invalid("Content-Length overflows the platform size"))?; + if size > MAX_BODY_BYTES { + return Err(invalid("MEP body exceeds 16 MiB")); + } + length = Some(size); + } + } + let length = length.ok_or_else(|| invalid("Missing Content-Length"))?; + let mut body = vec![0; length]; + reader.read_exact(&mut body)?; + std::str::from_utf8(&body).map_err(|_| invalid("MEP body must be UTF-8"))?; + Ok(Some(body)) +} + +/// Write and flush one bounded UTF-8 frame, using byte length rather than characters. +pub fn write_frame(writer: &mut impl Write, body: &[u8]) -> io::Result<()> { + if body.len() > MAX_BODY_BYTES { + return Err(invalid("MEP response exceeds 16 MiB")); + } + std::str::from_utf8(body).map_err(|_| invalid("MEP response must be UTF-8"))?; + write!(writer, "Content-Length: {}\r\n\r\n", body.len())?; + writer.write_all(body)?; + writer.flush() +} diff --git a/rust/crates/morphir-python-extension/src/lib.rs b/rust/crates/morphir-python-extension/src/lib.rs new file mode 100644 index 00000000..aa2df2e5 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/lib.rs @@ -0,0 +1,4 @@ +//! Bounded stdio transport for the Morphir Python process guest. +pub mod framing; +mod server; +pub use server::serve; diff --git a/rust/crates/morphir-python-extension/src/main.rs b/rust/crates/morphir-python-extension/src/main.rs new file mode 100644 index 00000000..03e22589 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/main.rs @@ -0,0 +1,27 @@ +//! Standalone process entry point; the compiler never imports or executes Python. +use morphir_python_extension::serve; + +fn main() { + let mut arguments = std::env::args_os().skip(1); + let argument = arguments.next(); + if arguments.next().is_some() { + eprintln!("Usage: morphir-python-extension --stdio"); + std::process::exit(2); + } + // Canonical native bundles launch their executable without arguments. + // Keep --stdio as an explicit alias for host configurations and manual use. + if argument.as_deref().is_none_or(|value| value == "--stdio") { + if let Err(error) = serve(&mut std::io::stdin().lock(), &mut std::io::stdout().lock()) { + eprintln!("morphir-python-extension: {error}"); + std::process::exit(1); + } + return; + } + match argument.as_deref() { + Some(value) if value == "--help" => println!("Usage: morphir-python-extension --stdio"), + _ => { + eprintln!("Usage: morphir-python-extension --stdio"); + std::process::exit(2); + } + } +} diff --git a/rust/crates/morphir-python-extension/src/server.rs b/rust/crates/morphir-python-extension/src/server.rs new file mode 100644 index 00000000..1017d056 --- /dev/null +++ b/rust/crates/morphir-python-extension/src/server.rs @@ -0,0 +1,114 @@ +//! Framed stdio sessions around the SDK's native protocol endpoint. +use crate::framing::{MAX_BODY_BYTES, read_frame, write_frame}; +use morphir_extension_sdk::{ + NativeExtension, + protocol::{ExtensionRequest, error_codes, methods}, +}; +use morphir_python_binding::PythonExtension; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::value::RawValue; +use std::io::{self, BufRead, Write}; + +#[derive(Deserialize)] +struct MessageHead { + jsonrpc: String, + method: String, + // Explicit null is a malformed request ID, not a notification. + #[serde(default, deserialize_with = "present_id")] + id: Option>, +} + +fn present_id<'de, D: Deserializer<'de>>(reader: D) -> Result>, D::Error> { + Box::::deserialize(reader).map(Some) +} + +fn write_error(writer: &mut impl Write, code: i32, message: &str) -> io::Result<()> { + write_response( + writer, + &serde_json::json!({ + "jsonrpc": "2.0", "id": null, "error": { "code": code, "message": message }, + }), + ) +} + +fn write_response(writer: &mut impl Write, value: &impl Serialize) -> io::Result<()> { + struct Body(Vec); + impl Write for Body { + fn write(&mut self, bytes: &[u8]) -> io::Result { + if self.0.len().saturating_add(bytes.len()) > MAX_BODY_BYTES { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "MEP response exceeds 16 MiB", + )); + } + self.0.extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + let mut body = Body(Vec::new()); + serde_json::to_writer(&mut body, value).map_err(io::Error::other)?; + write_frame(writer, &body.0) +} + +/// Serve a fresh SDK session, with protocol frames as the only output. +/// +/// Invalid JSON or request envelopes receive JSON-RPC errors without entering +/// the compiler. A damaged frame is fatal because its next boundary is unknown. +/// Notifications receive no response; exit stops processing further messages. +/// Every invocation owns an independent native SDK session. +pub fn serve(reader: &mut impl BufRead, writer: &mut impl Write) -> io::Result<()> { + let extension = NativeExtension::frontend_backend(PythonExtension) + .map_err(|error| io::Error::other(error.to_string()))?; + let session = extension.open_protocol(); + while let Some(body) = read_frame(reader)? { + // Borrow the raw document to validate syntax without materializing + // opaque parameters or giving serde-private-looking keys meaning. + if serde_json::from_slice::<&RawValue>(&body).is_err() { + write_error(writer, error_codes::PARSE_ERROR, "Invalid JSON")?; + continue; + } + let head = match serde_json::from_slice::(&body) { + Ok(head) if head.jsonrpc == "2.0" => head, + _ => { + write_error( + writer, + error_codes::INVALID_REQUEST, + "Invalid JSON-RPC request", + )?; + continue; + } + }; + if head.id.is_none() { + if head.method == methods::EXIT { + break; + } + // Initialized acknowledges a handshake already held by the SDK. + // Unknown or unadvertised notifications do no work. + continue; + } + let request = match serde_json::from_slice::(&body) { + Ok(request) => request, + Err(_) => { + write_error(writer, error_codes::INVALID_REQUEST, "Invalid MEP request")?; + continue; + } + }; + write_response(writer, &session.handle(request))?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn oversized_response_is_refused_before_any_frame_is_emitted() { + let mut output = Vec::new(); + let value = "x".repeat(MAX_BODY_BYTES); + assert!(write_response(&mut output, &value).is_err()); + assert!(output.is_empty()); + } +} diff --git a/rust/crates/morphir-python-extension/tests/framing.rs b/rust/crates/morphir-python-extension/tests/framing.rs new file mode 100644 index 00000000..619ff1e2 --- /dev/null +++ b/rust/crates/morphir-python-extension/tests/framing.rs @@ -0,0 +1,75 @@ +//! Stream boundaries, byte lengths and resource bounds are transport contracts. +use morphir_python_extension::framing::{ + MAX_BODY_BYTES, MAX_HEADER_BYTES, read_frame, write_frame, +}; +use std::io::{BufReader, Cursor}; + +#[test] +fn unicode_messages_and_adjacent_frames_survive_partial_reads() { + let mut wire = Vec::new(); + for text in [r#"{"id":"λ"}"#, r#"{"id":2}"#] { + write_frame(&mut wire, text.as_bytes()).unwrap(); + } + let mut input = BufReader::with_capacity(1, Cursor::new(wire)); + assert_eq!( + read_frame(&mut input).unwrap().unwrap(), + r#"{"id":"λ"}"#.as_bytes() + ); + assert_eq!( + read_frame(&mut input).unwrap().unwrap(), + r#"{"id":2}"#.as_bytes() + ); + assert_eq!(read_frame(&mut input).unwrap(), None); +} + +#[test] +fn malformed_or_truncated_input_is_refused() { + for wire in [ + b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}".as_slice(), + b"Content-Length: -1\r\n\r\n", + b"Content-Length: +2\r\n\r\n{}", + b"Content-Length: 184467440737095516160\r\n\r\n", + b"Other: value\r\n\r\n{}", + b"Content-Length: 2\r\n", + b"Content-Length: 2\r\n\r\n{", + b"Content-Length: 1\r\n\r\n\xff", + b"Content-Length: 2\r\nOther: invalid\nline\r\n\r\n{}", + b"Content-Length: 2\r\nBad Name: value\r\n\r\n{}", + ] { + assert!( + read_frame(&mut Cursor::new(wire)).is_err(), + "accepted {wire:?}" + ); + } +} + +#[test] +fn bounds_are_applied_before_reading_an_advertised_body() { + let huge = format!("Content-Length: {}\r\n\r\n", MAX_BODY_BYTES + 1); + assert!(read_frame(&mut Cursor::new(huge)).is_err()); + let huge_header = vec![b'x'; MAX_HEADER_BYTES + 1]; + assert!(read_frame(&mut Cursor::new(huge_header)).is_err()); + assert!(write_frame(&mut Vec::new(), &vec![b'x'; MAX_BODY_BYTES + 1]).is_err()); +} + +#[test] +fn content_length_is_case_insensitive_and_uses_bytes() { + let mut input = Cursor::new(b"content-length: 2\r\nContent-Type: application/json\r\n\r\n{}"); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), b"{}"); + let mut wire = Vec::new(); + write_frame(&mut wire, "λ".as_bytes()).unwrap(); + assert_eq!(wire, "Content-Length: 2\r\n\r\nλ".as_bytes()); +} + +#[test] +fn a_complete_header_at_the_limit_is_accepted() { + let prefix = "Content-Length: 2\r\nOther: "; + let suffix = "\r\n\r\n"; + let header = format!( + "{prefix}{}{suffix}", + "x".repeat(MAX_HEADER_BYTES - prefix.len() - suffix.len()) + ); + assert_eq!(header.len(), MAX_HEADER_BYTES); + let mut input = Cursor::new(format!("{header}{{}}")); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), b"{}"); +} diff --git a/rust/crates/morphir-python-extension/tests/session.rs b/rust/crates/morphir-python-extension/tests/session.rs new file mode 100644 index 00000000..75085420 --- /dev/null +++ b/rust/crates/morphir-python-extension/tests/session.rs @@ -0,0 +1,253 @@ +//! A real process and in-memory streams share the same SDK lifecycle. +use morphir_python_extension::{ + framing::{read_frame, write_frame}, + serve, +}; +use serde_json::{Value, json}; +use std::io::{Cursor, Write}; +use std::process::{Command, Stdio}; + +fn wire(messages: &[Value]) -> Vec { + let mut bytes = Vec::new(); + for message in messages { + write_frame(&mut bytes, &serde_json::to_vec(message).unwrap()).unwrap(); + } + bytes +} +fn responses(bytes: Vec) -> Vec { + let mut input = Cursor::new(bytes); + let mut responses = Vec::new(); + while let Some(body) = read_frame(&mut input).unwrap() { + responses.push(serde_json::from_slice(&body).unwrap()); + } + responses +} +fn initialize() -> Value { + json!({"jsonrpc":"2.0","id":1,"method":"morphir.initialize","params":{"protocolVersions":["0.1"],"host":{"name":"test","version":"1"}}}) +} + +#[test] +fn sessions_preserve_lifecycle_ids_and_notification_silence() { + let messages = [ + json!({"jsonrpc":"2.0","id":0,"method":"morphir.extension.info","params":{}}), + initialize(), + json!({"jsonrpc":"2.0","method":"morphir.initialized"}), + json!({"jsonrpc":"2.0","method":"unknown.notification","params":{"text":"λ"}}), + json!({"jsonrpc":"2.0","id":2,"method":"morphir.ping","params":{}}), + json!({"jsonrpc":"2.0","id":3,"method":"morphir.shutdown","params":{}}), + json!({"jsonrpc":"2.0","id":4,"method":"morphir.extension.info","params":{}}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + json!({"jsonrpc":"2.0","id":5,"method":"morphir.ping","params":{}}), + ]; + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 5); + assert_eq!( + replies + .iter() + .map(|value| value["id"].as_u64().unwrap()) + .collect::>(), + [0, 1, 2, 3, 4] + ); + assert_eq!(replies[0]["error"]["code"], -32014); + assert_eq!(replies[1]["result"]["protocolVersion"], "0.1"); + assert_eq!(replies[2]["result"]["ok"], true); + assert_eq!(replies[4]["error"]["code"], -32014); +} + +#[test] +fn invalid_json_is_refused_and_the_next_framed_request_can_recover() { + let mut input = Vec::new(); + write_frame(&mut input, b"{invalid").unwrap(); + input.extend(wire(&[ + json!({"jsonrpc":"2.0","id":7,"method":"morphir.ping","params":{}}), + ])); + let mut output = Vec::new(); + serve(&mut Cursor::new(input), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 2); + assert_eq!(replies[0]["error"]["code"], -32700); + assert_eq!(replies[0]["id"], Value::Null); + assert_eq!(replies[1]["id"], 7); +} + +#[test] +fn malformed_frames_fail_without_writing_non_protocol_output() { + let mut output = Vec::new(); + assert!( + serve( + &mut Cursor::new(b"Content-Length: 999999999\r\n\r\n"), + &mut output + ) + .is_err() + ); + assert!(output.is_empty()); +} + +#[test] +fn standalone_process_needs_no_python_or_rust_environment() { + standalone(&["--stdio"]); + standalone(&[]); +} + +fn standalone(arguments: &[&str]) { + let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) + .args(arguments) + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let input = wire(&[ + initialize(), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ]); + child.stdin.take().unwrap().write_all(&input).unwrap(); + let result = child.wait_with_output().unwrap(); + assert!(result.status.success(), "{:?}", result.stderr); + assert!(result.stderr.is_empty()); + let replies = responses(result.stdout); + assert_eq!(replies.len(), 1); + assert_eq!(replies[0]["result"]["extension"]["id"], "morphir-python"); +} + +#[test] +fn invalid_request_ids_do_not_become_notifications_or_poison_the_session() { + let mut messages = Vec::new(); + for id in [ + Value::Null, + json!(true), + json!([]), + json!({}), + json!(-1), + json!(1.5), + ] { + messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping","params":{}})); + } + messages.push(json!({"jsonrpc":"2.0","id":77,"method":"morphir.ping","params":{}})); + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 7); + for reply in &replies[..6] { + assert_eq!(reply["id"], Value::Null); + assert_eq!(reply["error"]["code"], -32600); + } + assert_eq!(replies[6]["id"], 77); + assert_eq!(replies[6]["result"]["ok"], true); +} + +#[test] +fn independent_stream_sessions_cannot_share_initialization_state() { + let mut first = Vec::new(); + serve(&mut Cursor::new(wire(&[initialize()])), &mut first).unwrap(); + assert_eq!(responses(first)[0]["result"]["protocolVersion"], "0.1"); + let mut second = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + json!({"jsonrpc":"2.0","id":2,"method":"morphir.extension.info","params":{}}), + ])), + &mut second, + ) + .unwrap(); + assert_eq!(responses(second)[0]["error"]["code"], -32014); +} + +#[test] +fn canonical_numeric_and_string_ids_survive_requests_and_lifecycle() { + let ids = [json!(u64::MAX), json!("λ/request"), json!(""), json!(0)]; + let mut messages = vec![initialize()]; + for id in &ids { + messages.push(json!({"jsonrpc":"2.0","id":id,"method":"morphir.ping"})); + } + messages.extend([ + json!({"jsonrpc":"2.0","id":"shutdown","method":"morphir.shutdown","params":{}}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ]); + let mut output = Vec::new(); + serve(&mut Cursor::new(wire(&messages)), &mut output).unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 6); + for (reply, id) in replies[1..5].iter().zip(ids) { + assert_eq!(reply["id"], id); + assert_eq!(reply["result"]["ok"], true); + } + assert_eq!(replies[5]["id"], "shutdown"); + assert!(replies[5].get("error").is_none()); +} + +#[test] +fn canonical_empty_parameter_requests_need_no_params_member() { + let mut output = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + initialize(), + json!({"jsonrpc":"2.0","id":2,"method":"morphir.ping"}), + json!({"jsonrpc":"2.0","id":3,"method":"morphir.shutdown"}), + json!({"jsonrpc":"2.0","method":"morphir.exit"}), + ])), + &mut output, + ) + .unwrap(); + let replies = responses(output); + assert_eq!(replies[1]["id"], 2); + assert_eq!(replies[1]["result"]["ok"], true); + assert_eq!(replies[2]["id"], 3); + assert!(replies[2].get("error").is_none()); +} + +#[test] +fn truncated_input_exits_with_diagnostics_only_on_stderr() { + let mut child = Command::new(env!("CARGO_BIN_EXE_morphir-python-extension")) + .env_clear() + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(b"Content-Length: 10\r\n\r\n{}") + .unwrap(); + let result = child.wait_with_output().unwrap(); + assert_eq!(result.status.code(), Some(1)); + assert!(result.stdout.is_empty()); + assert!( + String::from_utf8(result.stderr) + .unwrap() + .contains("morphir-python-extension:") + ); +} + +#[test] +fn description_is_fixed_across_initialization_and_reinitialization_refusal() { + let describe = |id| json!({"jsonrpc":"2.0","id":id,"method":"morphir.extension.describe","params":{"protocolVersions":["0.1"]}}); + let mut second_init = initialize(); + second_init["id"] = json!(3); + second_init["params"]["protocolVersions"] = json!(["0.2.0-draft.99"]); + let mut output = Vec::new(); + serve( + &mut Cursor::new(wire(&[ + describe(0), + initialize(), + describe(2), + second_init, + describe(4), + ])), + &mut output, + ) + .unwrap(); + let replies = responses(output); + assert_eq!(replies.len(), 5); + assert_eq!( + replies[0]["result"]["protocolVersions"], + json!(["0.2.0-draft.1", "0.1"]) + ); + assert_eq!(replies[0]["result"], replies[2]["result"]); + assert_eq!(replies[2]["result"], replies[4]["result"]); + assert_eq!(replies[3]["error"]["code"], -32600); +} diff --git a/rust/crates/morphir-python-native/Cargo.toml b/rust/crates/morphir-python-native/Cargo.toml index c6b9735d..f3de8815 100644 --- a/rust/crates/morphir-python-native/Cargo.toml +++ b/rust/crates/morphir-python-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-native" -version = "0.1.1" +version = "0.1.2" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/rust/extension.toml b/rust/extension.toml index 6f3dcf62..d2a48cad 100644 --- a/rust/extension.toml +++ b/rust/extension.toml @@ -9,7 +9,7 @@ package = "morphir-python-binding" artifact = "morphir-python-binding" extension_id = "morphir-python" name = "Morphir Python" -mep_versions = ["0.1"] +mep_versions = ["0.2.0-draft.1", "0.1"] targets = ["python"] languages = [{ id = "python", file_extensions = [".py"] }] ir_versions = ["3", "4"] @@ -32,3 +32,19 @@ release = "https://github.com/finos/morphir/releases/download/v0.4.0-beta.10" "aarch64-unknown-linux-gnu" = "76a92a11030cd254d2283e98363cf203f86aaecd583fa480bdf6f508c86710f6" "aarch64-apple-darwin" = "98635ec74fbdc3c2cef1d4ae184bb1c5ed9e2960575f3d8abb4b97e0e4da363f" "x86_64-apple-darwin" = "4080dcfb04f6e1c902bce2780756c6af44320ac0dd6ff6bd953ca74a0da8e128" + +"x86_64-pc-windows-msvc" = "c178002d570f4cb8c9ceed4bf7b51f04d4ebec2458765935839c90ac1cc01176" + +# Shared executable conformance, independently authored in finos/morphir. +# This source is merged; the Python and MoonBit profiles remain separate. +[mep] +repository = "https://github.com/finos/morphir.git" +rev = "85cf4352eeb517fefb85cda10961f749cce51004" +tree = "c42c55ef3aa9d0190850251dac97bd094cebb4c4" +cargo_lock_sha256 = "b502b3c627b4b2b8f5835f5ad56d9f341ad24722471c75113f49688b39e56f9b" +runner_version = "0.4.0-beta.10" +kit = "spec/mep/mck/guest-python-json.json" +profile = "python-json-library-v1" +corpus_sha256 = "9b4b5079ec36275b6ab5f11e7029e40959bc6577dcae41c4659d4151fab45a9d" +required_cases = 91 +case_set_sha256 = "a1d451ed13bee322b574637d74548d36e3846205a88b4e1830a9a5bf708a7264" diff --git a/scripts/benchmark_extension.py b/scripts/benchmark_extension.py new file mode 100644 index 00000000..9f8d0e74 --- /dev/null +++ b/scripts/benchmark_extension.py @@ -0,0 +1,226 @@ +"""Measure fresh guests and ordered v3/v4 operations using the real Rust host. + +The executable checker verifies outputs and diagnostics before accepting a sample. +This is a local performance observation, not canonical MEP conformance evidence. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import statistics +import subprocess +import time +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] + + +def sha(path: Path) -> str: + """Hash retained input, checker, artifact or result bytes.""" + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def sources(records: int, functions: int) -> tuple[str, str]: + """Create frozen records and typed conditional functions deterministically.""" + + def suffix(index: int) -> str: + # Classic v3 word arrays cannot preserve numeric identifier parts. + result = "" + while True: + result = chr(ord("a") + index % 26) + result + index = index // 26 - 1 + if index < 0: + return result + + types = "from dataclasses import dataclass\n" + "".join( + f"\n@dataclass(frozen=True)\nclass Item{suffix(index).capitalize()}:\n" + " value: int\n" + for index in range(records) + ) + values = types + "".join( + f"\ndef choose_{suffix(index)}(flag: bool, first: int, second: int) -> int:\n" + " return first if flag else second\n" + for index in range(functions) + ) + return types, values + + +def summarize(observations: dict[str, list[int]]) -> dict[str, Any]: + """Retain raw nanoseconds with descriptive statistics and no speed claim.""" + return { + name: { + "samplesNs": values, + "minNs": min(values), + "medianNs": statistics.median(values), + "maxNs": max(values), + } + for name, values in observations.items() + } + + +def benchmark( + checker: Path, wasm: Path, process: Path, output: Path, samples: int +) -> None: + """Run fresh checker processes, verify identities, and save all raw results.""" + checker, wasm, process = ( + path.resolve(strict=True) for path in (checker, wasm, process) + ) + output.mkdir(parents=True, exist_ok=False) + expected = {"wasmSha256": sha(wasm), "processSha256": sha(process)} + definitions = { + name: sha(ROOT / "tools/extension-qualification" / name) + for name in ("Cargo.toml", "Cargo.lock", "build.rs", "src/main.rs") + } + checker_digest = sha(checker) + host_metadata: dict[str, Any] = {} + reports: list[dict[str, Any]] = [] + measurements: dict[str, list[int]] = {} + for name, record_count, function_count in (("small", 1, 1), ("medium", 200, 100)): + directory = output / name + directory.mkdir() + types, values = sources(record_count, function_count) + type_path, value_path = directory / "types.py", directory / "values.py" + type_path.write_text(types) + value_path.write_text(values) + inputs = { + "typesOnlySha256": sha(type_path), + "typesAndValuesSha256": sha(value_path), + } + for index in range(samples): + report = directory / f"sample-{index}.json" + started = time.perf_counter_ns() + result = subprocess.run( + [ + str(checker), + str(wasm), + str(process), + str(report), + str(type_path), + str(value_path), + ], + capture_output=True, + text=True, + timeout=120, + ) + elapsed = time.perf_counter_ns() - started + (directory / f"sample-{index}.log").write_text( + result.stdout + result.stderr + ) + if result.returncode: + raise RuntimeError(f"Checker failed: {result.stderr}") + raw = json.loads(report.read_text()) + if ( + raw.get("scope") != "source-pinned-legacy-host-interoperability" + or raw.get("checkerBinarySha256") != checker_digest + or raw.get("inputs") != inputs + or raw.get("definitionInputs") != definitions + or any(raw.get(key) != value for key, value in expected.items()) + ): + raise ValueError( + "Checker report does not match the measured inputs/bytes" + ) + observed_host = {key: raw[key] for key in ("hostSource", "rustc", "build")} + if host_metadata and host_metadata != observed_host: + raise ValueError( + "Measured host or build settings changed between samples" + ) + host_metadata = observed_host + measurements.setdefault(f"{name}/checker-wall", []).append(elapsed) + for runtime in raw["results"]: + prefix = f"{name}/{runtime['runtime']}" + measurements.setdefault(prefix + "/fresh-guest-initialize", []).append( + runtime["startupNs"] + ) + for operation in runtime["operations"]: + kind = "types" if operation["typesOnly"] else "values" + key = f"{prefix}/v{operation['irVersion']}/{kind}" + for phase in ("compile", "generate"): + measurements.setdefault(key + "/" + phase, []).append( + operation[phase + "Ns"] + ) + reports.append( + { + "path": report.relative_to(output).as_posix(), + "sha256": sha(report), + "stdout": result.stdout, + } + ) + if sha(checker) != checker_digest or any( + sha(path) != expected[key] + for path, key in ((wasm, "wasmSha256"), (process, "processSha256")) + ): + raise ValueError("Measured checker or artifact changed during execution") + summary = { + "scope": "local-legacy-runtime-timings", + "state": "succeeded", + "samplesPerWorkload": samples, + "method": { + "host": "Fresh checker process per sample; WASM then process, sequentially", + "startup": ( + "Fresh guest creation and MEP initialization; " + "WASM includes module compilation" + ), + "operations": ( + "One session per runtime: v3 types, v3 values, v4 types, v4 values; " + "no guest restart between operations" + ), + "cache": ( + "OS filesystem caches are not cleared; " + "no persistent Wasmtime module cache is configured" + ), + "validation": ( + "Every sample checks compile/generate/recompile semantics, runtime " + "diagnostic parity, orderly shutdown and process timeout cleanup" + ), + "limits": ( + "One machine/toolchain and fixed runtime order; " + "descriptive measurements only, not a general speed claim" + ), + }, + "environment": { + "platform": platform.platform(), + "machine": platform.machine(), + "processor": platform.processor(), + "logicalCpus": os.cpu_count(), + "python": platform.python_version(), + }, + "checkerBinarySha256": checker_digest, + **host_metadata, + "artifacts": expected, + "definitionInputs": { + name: sha(ROOT / name) + for name in ( + "scripts/benchmark_extension.py", + "tools/extension-qualification/Cargo.toml", + "tools/extension-qualification/Cargo.lock", + "tools/extension-qualification/build.rs", + "tools/extension-qualification/src/main.rs", + ) + }, + "reports": reports, + "measurements": summarize(measurements), + } + (output / "timings.json").write_text(json.dumps(summary, indent=2) + "\n") + print( + f"Measured {samples} samples of each workload; report {output / 'timings.json'}" + ) + + +def main() -> None: + """Accept exact built artifacts and a new destination for retained evidence.""" + parser = argparse.ArgumentParser(description=__doc__) + for name in ("checker", "wasm", "process", "output"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--samples", type=int, default=5, choices=range(3, 31)) + args = parser.parse_args() + benchmark(args.checker, args.wasm, args.process, args.output, args.samples) + + +if __name__ == "__main__": + main() diff --git a/scripts/extension_bundle.py b/scripts/extension_bundle.py index 53f9699e..eb053b6b 100644 --- a/scripts/extension_bundle.py +++ b/scripts/extension_bundle.py @@ -25,13 +25,16 @@ import json import os import platform +import re import shutil +import stat import subprocess import sys import tarfile import tempfile import tomllib import urllib.request +import zipfile from pathlib import Path from typing import Any @@ -220,9 +223,11 @@ def receipt(bundle: Path, *, version: str | None = None) -> dict[str, object]: def _target() -> str: machine = platform.machine().lower() arch = {"arm64": "aarch64", "amd64": "x86_64"}.get(machine, machine) - system = {"Darwin": "apple-darwin", "Linux": "unknown-linux-gnu"}.get( - platform.system() - ) + system = { + "Darwin": "apple-darwin", + "Linux": "unknown-linux-gnu", + "Windows": "pc-windows-msvc", + }.get(platform.system()) if system is None: raise BundleError(f"no pinned Morphir CLI for {platform.system()}") return f"{arch}-{system}" @@ -236,16 +241,29 @@ def morphir_cli() -> tuple[Path, str]: if expected is None: raise BundleError(f"no pinned Morphir CLI digest for {target}") directory = CACHE / f"morphir-{host['version']}-{target}" - binary = directory / "morphir" - archive_name = f"morphir-{host['version']}-{target}.tgz" - if not binary.is_file(): + is_windows = target.endswith("windows-msvc") + binary = directory / ("morphir.exe" if is_windows else "morphir") + extension = "zip" if is_windows else "tgz" + archive_name = f"morphir-{host['version']}-{target}.{extension}" + archive_path = directory / archive_name + if archive_path.is_symlink() or binary.is_symlink(): + raise BundleError("cached Morphir CLI and archive must be regular files") + if archive_path.is_file(): + data = archive_path.read_bytes() + else: with urllib.request.urlopen( f"{host['release']}/{archive_name}", timeout=120 - ) as r: - data = r.read() - if sha256(data) != expected: - raise BundleError(f"{archive_name} digest differs from the pin") - directory.mkdir(parents=True, exist_ok=True) + ) as response: + data = response.read() + if sha256(data) != expected: + raise BundleError(f"{archive_name} digest differs from the pin") + if is_windows: + with zipfile.ZipFile(io.BytesIO(data)) as archive: + zip_member = next( + m for m in archive.namelist() if Path(m).name == "morphir.exe" + ) + executable_bytes = archive.read(zip_member) + else: with tarfile.open(fileobj=io.BytesIO(data)) as archive: member = next( m for m in archive.getmembers() if Path(m.name).name == "morphir" @@ -253,17 +271,124 @@ def morphir_cli() -> tuple[Path, str]: extracted = archive.extractfile(member) if extracted is None: raise BundleError(f"{archive_name} has no morphir executable") - binary.write_bytes(extracted.read()) - binary.chmod(0o755) + executable_bytes = extracted.read() + if binary.exists() and binary.read_bytes() != executable_bytes: + raise BundleError("cached Morphir CLI differs from the verified archive") + directory.mkdir(parents=True, exist_ok=True) + if not archive_path.exists(): + archive_path.write_bytes(data) + if not binary.exists(): + binary.write_bytes(executable_bytes) + binary.chmod(0o755) return binary, expected +def is_digest_mismatch(text: str, expected: str, observed: str) -> bool: + """Require the verifier's mismatch and both exact hashes, allowing wrapping.""" + compact = re.sub(r"[\s│╰─]", "", text.lower()) + return "digestmismatch" in compact and expected in compact and observed in compact + + +def check_installed_integrity( + cli: Path, home: Path, environment: dict[str, str] +) -> dict[str, Any]: + """Refuse changed installed bytes in a fresh project, then restore them.""" + catalog = json.loads((home / "catalog/extensions.json").read_text()) + (installed,) = catalog["extensions"] + stored = home / installed["storePath"] + if ( + stored.is_symlink() + or not stored.is_file() + or not stored.resolve().is_relative_to(home.resolve()) + ): + raise BundleError("integrity fixture must be a regular file inside its home") + original = stored.read_bytes() + expected = sha256(original) + if installed["digest"] != expected: + raise BundleError("installed artifact already disagrees with its catalog") + modified = bytearray(original) + modified[-1] ^= 1 + observed = sha256(bytes(modified)) + mode = stat.S_IMODE(stored.stat().st_mode) + + def replace_bytes(data: bytes) -> None: + # Replacing the directory entry preserves any hardlinked cache or input. + with tempfile.NamedTemporaryFile(dir=stored.parent, delete=False) as file: + file.write(data) + replacement = Path(file.name) + try: + replacement.chmod(mode) + replacement.replace(stored) + finally: + replacement.unlink(missing_ok=True) + + with tempfile.TemporaryDirectory(prefix="morphir-integrity-") as temporary: + project = Path(temporary) + shutil.copytree(SAMPLE, project / "src") + (project / "morphir.toml").write_text( + '[project]\nname = "acme/example"\nversion = "0.1.0"\n' + 'source_directory = "src"\n[frontend]\nlanguage = "python"\n' + "emit_parse_stage = false\n" + ) + env = {**environment, "MORPHIR_HOME": str(home)} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + env[name] = env[name.lower()] = OFFLINE + env.pop("NO_PROXY", None) + env.pop("no_proxy", None) + command = [ + str(cli), + "--no-banner", + "compile", + "--language", + "python", + "--input", + "src", + "--ir-version", + "4", + "--output", + "must-not-exist", + ] + replace_bytes(bytes(modified)) + try: + result = subprocess.run( + command, + cwd=project, + env=env, + capture_output=True, + text=True, + timeout=60, + ) + reason = result.stdout + result.stderr + if ( + result.returncode == 0 + or not is_digest_mismatch(reason, expected, observed) + or (project / "must-not-exist").exists() + ): + raise BundleError( + f"installed artifact integrity was not enforced: {reason}" + ) + finally: + replace_bytes(original) + return { + "expected_sha256": expected, + "tampered_sha256": observed, + "exit_code": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + "fresh_project": True, + } + + def host_check(bundle: Path, *, version: str | None = None) -> dict[str, object]: """Install the bundle with the pinned CLI and round trip offline.""" + return host_round_trips(bundle, receipt(bundle, version=version)) + + +def host_round_trips(bundle: Path, described: dict[str, Any]) -> dict[str, object]: + """Qualify an admitted runtime bundle through installed, offline CLI routes.""" from morphir_tools.python import native_compiler bundle = bundle.resolve() - described = receipt(bundle, version=version) cli, cli_digest = morphir_cli() with tempfile.TemporaryDirectory(prefix="python-bundle-host-") as scratch: work = Path(scratch) @@ -290,6 +415,7 @@ def morphir(*args: str, offline: bool = False) -> str: capture_output=True, text=True, check=False, + timeout=60, ) if result.returncode != 0: raise BundleError(f"morphir {' '.join(args)}: {result.stderr}") @@ -334,13 +460,27 @@ def morphir(*args: str, offline: bool = False) -> str: ) results[f"v{tag}"] = { "modules": files, + "ir_sha256": sha256( + json.dumps(first, sort_keys=True, separators=(",", ":")).encode() + ), + "generated_sha256": { + path: sha256((project / generated / path).read_bytes()) + for path in files + }, "agrees_with_python_api": agrees_with_api( first, tag, compiler_version=str(described["version"]) ), } + integrity = check_installed_integrity(cli, home, base) return { **described, - "host": {"morphir": declaration()["host"]["version"], "sha256": cli_digest}, + "installed_integrity": integrity, + "host": { + "morphir": declaration()["host"]["version"], + "sha256": cli_digest, + "binary_sha256": sha256(cli.read_bytes()), + }, + "qualification_script_sha256": sha256(Path(__file__).read_bytes()), "python_api": { "native_version": native_compiler().version, "compiler_version": native_compiler().compiler_version, diff --git a/scripts/mep_tool.py b/scripts/mep_tool.py new file mode 100644 index 00000000..a61fe741 --- /dev/null +++ b/scripts/mep_tool.py @@ -0,0 +1,212 @@ +"""Build the exact shared MCK source without changing its frozen expectations.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + + +def sha(path: Path) -> str: + """Hash available file bytes without loading a checker binary into memory.""" + with path.open("rb") as stream: + return hashlib.file_digest(stream, "sha256").hexdigest() + + +def object_file(path: Path) -> dict[str, Any]: + """Read a required JSON object at the tool boundary.""" + value = json.loads(path.read_bytes()) + if not isinstance(value, dict): + raise bundle.BundleError(f"expected an object: {path}") + return value + + +def freeze_binary(executable: Path, cache: Path) -> Path: + """Keep report identities independent of mutable Cargo output filenames.""" + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("MCK build output must be a regular file") + digest = sha(executable) + frozen = cache / digest / executable.name + frozen.parent.mkdir(parents=True, exist_ok=True) + if frozen.exists() or frozen.is_symlink(): + if frozen.is_symlink() or not frozen.is_file() or sha(frozen) != digest: + raise bundle.BundleError("stored MCK checker integrity differs") + return frozen + created = False + try: + with frozen.open("xb") as destination, executable.open("rb") as source: + created = True + shutil.copyfileobj(source, destination) + if sha(frozen) != digest: + raise bundle.BundleError("MCK checker copy integrity differs") + if os.name != "nt": + frozen.chmod(0o700) + except BaseException: + if created: + frozen.unlink(missing_ok=True) + raise + return frozen + + +def command(arguments: list[str], *, cwd: Path | None = None) -> str: + """Run a bounded setup command, retaining diagnostics on failure.""" + result = subprocess.run( + arguments, + cwd=cwd, + text=True, + capture_output=True, + timeout=180, + ) + if result.returncode: + raise bundle.BundleError(f"{arguments[0]} failed: {result.stderr}") + return result.stdout.strip() + + +def configure_checkout(checkout: Path) -> None: + """Keep parent and submodule byte/path handling consistent, without resets.""" + command(["git", "config", "core.autocrlf", "false"], cwd=checkout) + command(["git", "config", "core.longpaths", "true"], cwd=checkout) + + +def build_checker(source: Path, target: Path, log: Path) -> None: + """Build with bounded CI diagnostics and preserve the complete raw log.""" + environment = os.environ | { + "CARGO_PROFILE_DEV_DEBUG": "0", + "CARGO_INCREMENTAL": "0", + } + if cache_home := environment.get("MORPHIR_MCK_CARGO_HOME"): + # Cargo's Git checkout includes a 192-character Gleam snapshot path. + # Windows CI uses RUNNER_TEMP/mck rather than the longer user profile. + environment["CARGO_HOME"] = str(Path(cache_home).resolve()) + with log.open("wb") as stream: + result = subprocess.run( + [ + "cargo", + "build", + "--locked", + "--manifest-path", + str(source / "Cargo.toml"), + "--target-dir", + str(target), + "-p", + "morphir", + ], + cwd=bundle.ROOT, + env=environment, + stdout=stream, + stderr=subprocess.STDOUT, + timeout=1800, + ) + if result.returncode: + tail = "\n".join( + log.read_bytes().decode("utf-8", errors="replace").splitlines()[-80:] + ) + raise bundle.BundleError(f"MCK build failed; full log {log}\n{tail[-16000:]}") + + +def prepare(output: Path, target: Path | None = None) -> dict[str, Any]: + """Fetch source by Git identity and run Cargo even when a build cache exists. + + Cargo owns compatible object reuse. A cached receipt alone never authorizes + execution. Only the needed Rust submodule is initialized. + """ + pin = bundle.declaration()["mep"] + cache = bundle.ROOT / ".dev/mep-tool" / pin["rev"] + source = cache / "source" + source.mkdir(parents=True, exist_ok=True) + if not (source / ".git").is_dir(): + command(["git", "init", str(source)]) + command(["git", "remote", "add", "origin", pin["repository"]], cwd=source) + command(["git", "config", "core.autocrlf", "false"], cwd=source) + command(["git", "config", "core.longpaths", "true"], cwd=source) + if command(["git", "remote", "get-url", "origin"], cwd=source) != pin["repository"]: + raise bundle.BundleError("MCK source cache has another repository") + configure_checkout(source) + rust_source = source / "ecosystem/morphir-rust" + if (rust_source / ".git").exists(): + configure_checkout(rust_source) + command(["git", "fetch", "--depth", "1", "origin", pin["rev"]], cwd=source) + # Refuse tracked/untracked modifications rather than resetting somebody's work. + if command(["git", "status", "--porcelain"], cwd=source): + raise bundle.BundleError("MCK source cache is dirty") + command(["git", "checkout", "--detach", pin["rev"]], cwd=source) + if command(["git", "rev-parse", "HEAD^{tree}"], cwd=source) != pin["tree"]: + raise bundle.BundleError("MCK source tree differs from the audited pin") + command( + [ + "git", + "-c", + "core.autocrlf=false", + "-c", + "core.longpaths=true", + "submodule", + "update", + "--init", + "--depth", + "1", + "ecosystem/morphir-rust", + ], + cwd=source, + ) + configure_checkout(rust_source) + state = command(["git", "status", "--porcelain"], cwd=source) + if state: + raise bundle.BundleError( + f"MCK checkout is not clean before compilation:\n{state}" + ) + kit = source / pin["kit"] + if ( + sha(source / "Cargo.lock") != pin["cargo_lock_sha256"] + or sha(kit) != pin["corpus_sha256"] + ): + raise bundle.BundleError("MCK lockfile or frozen corpus differs") + command(["mise", "trust", str(source / ".config/mise/config.toml")]) + target = (target or cache / "target").resolve() + log = cache / "build.log" + # This is a correctness checker, not the implementation being benchmarked. + # A dev-profile build reduces setup cost and is identified in the receipt. + build_checker(source, target, log) + state = command(["git", "status", "--porcelain"], cwd=source) + if state: + raise bundle.BundleError(f"MCK build changed its pinned source:\n{state}") + executable = target / "debug" / ("morphir.exe" if os.name == "nt" else "morphir") + executable = freeze_binary(executable, cache / "bin") + receipt = { + "kind": "source-built-shared-mck", + "revision": pin["rev"], + "tree": pin["tree"], + "executable": str(executable), + "binarySha256": sha(executable), + "kit": str(kit), + "corpusSha256": sha(kit), + "cargoLockSha256": sha(source / "Cargo.lock"), + "profile": "dev", + "debugInfo": False, + "incremental": False, + "cargoHomeOverride": os.environ.get("MORPHIR_MCK_CARGO_HOME") or None, + "rustc": command(["rustc", "-vV"]), + "preparerSha256": sha(Path(__file__)), + } + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(receipt, indent=2) + "\n") + return receipt + + +def main() -> None: + """Prepare one compatible checker for explicit conformance consumers.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--target-dir", type=Path) + args = parser.parse_args() + prepare(args.output, args.target_dir) + + +if __name__ == "__main__": + main() diff --git a/scripts/process_diagnostics.py b/scripts/process_diagnostics.py new file mode 100644 index 00000000..cf9eaf92 --- /dev/null +++ b/scripts/process_diagnostics.py @@ -0,0 +1,203 @@ +"""Compare framed legacy process diagnostics with the exact installed Python API. + +This producer check supplements installed CLI checks, whose readable errors omit +language diagnostic codes. It is not canonical executable MEP conformance. +""" + +from __future__ import annotations + +import dataclasses +import json +import os +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + +SOURCE = "import os\n" + + +def _frame(message: dict[str, Any]) -> bytes: + body = json.dumps(message, ensure_ascii=False).encode("utf-8") + return f"Content-Length: {len(body)}\r\n\r\n".encode() + body + + +def _responses(data: bytes) -> list[dict[str, Any]]: + responses: list[dict[str, Any]] = [] + offset = 0 + while offset < len(data): + end = data.find(b"\r\n\r\n", offset, offset + 8192) + if end < 0: + raise bundle.BundleError("process diagnostic stdout has no bounded header") + lengths: list[int] = [] + for line in data[offset:end].split(b"\r\n"): + name, separator, value = line.partition(b":") + if not separator: + raise bundle.BundleError("process diagnostic stdout is not framed") + if name.lower() == b"content-length": + value = value.strip() + if not value.isdigit(): + raise bundle.BundleError( + "process diagnostic frame length is invalid" + ) + lengths.append(int(value)) + if len(lengths) != 1 or not 0 < lengths[0] <= 16 * 1024 * 1024: + raise bundle.BundleError("process diagnostic frame length is not bounded") + offset = end + 4 + body_end = offset + lengths[0] + if body_end > len(data): + raise bundle.BundleError("process diagnostic frame is truncated") + message = json.loads(data[offset:body_end].decode("utf-8")) + if not isinstance(message, dict) or len(responses) == 4: + raise bundle.BundleError( + "process diagnostic stdout has unexpected messages" + ) + responses.append(message) + offset = body_end + return responses + + +def qualify(executable: Path) -> dict[str, Any]: + """Check a real clean-environment session and retain structured parity proof.""" + from morphir_tools.python import ( + CompileFailure, + IrVersion, + SourceDocument, + compile_sources, + native_compiler, + ) + + native = native_compiler() + if native.compiler_version != bundle.crate_version(): + raise bundle.BundleError("diagnostic API compiler does not match candidate") + messages: list[dict[str, Any]] = [ + { + "jsonrpc": "2.0", + "id": 1, + "method": "morphir.initialize", + "params": { + "protocolVersions": ["0.1"], + "host": {"name": "python-diagnostic-qualification", "version": "1"}, + }, + }, + {"jsonrpc": "2.0", "method": "morphir.initialized"}, + ] + expected: dict[str, list[dict[str, Any]]] = {} + for number, version in enumerate(IrVersion, 2): + compiled = compile_sources( + [SourceDocument("models.py", SOURCE)], package="acme", ir_version=version + ) + if not isinstance(compiled, CompileFailure): + raise bundle.BundleError("diagnostic fixture unexpectedly compiled") + expected[version.value] = [ + { + "code": item.code, + "severity": item.severity.value, + "message": item.message, + "location": None + if item.source is None + else { + "uri": item.source.path, + "range": { + "start": dataclasses.asdict(item.source.start), + "end": dataclasses.asdict(item.source.end), + }, + }, + } + for item in compiled.diagnostics + ] + messages.append( + { + "jsonrpc": "2.0", + "id": number, + "method": "morphir.frontend.compile", + "params": { + "languageId": "python", + "sources": { + "documents": [ + { + "uri": "models.py", + "languageId": "python", + "version": 1, + "text": SOURCE, + } + ] + }, + "package": {"name": "acme"}, + "dependencies": [], + "options": {"irVersion": version.value, "typesOnly": False}, + }, + } + ) + messages.extend( + [ + {"jsonrpc": "2.0", "id": 4, "method": "morphir.shutdown", "params": {}}, + {"jsonrpc": "2.0", "method": "morphir.exit"}, + ] + ) + digest = bundle.sha256(executable.read_bytes()) + with tempfile.TemporaryDirectory(prefix="python-process-diagnostics-") as name: + work = Path(name) + owned = work / executable.name + owned.write_bytes(executable.read_bytes()) + if bundle.sha256(owned.read_bytes()) != digest: + raise bundle.BundleError("diagnostic artifact copy differs") + if os.name != "nt": + owned.chmod(0o700) + result = subprocess.run( + [str(owned)], + input=b"".join(map(_frame, messages)), + cwd=work, + env={}, + capture_output=True, + timeout=60, + ) + if result.returncode or result.stderr: + raise bundle.BundleError(f"diagnostic guest failed: {result.stderr!r}") + replies = _responses(result.stdout) + if [item.get("id") for item in replies] != [1, 2, 3, 4] or any( + item.get("jsonrpc") != "2.0" or "error" in item for item in replies + ): + raise bundle.BundleError("diagnostic guest lifecycle/IDs differ") + initialized = replies[0]["result"] + if not ( + initialized["protocolVersion"] == "0.1" + and initialized["extension"]["id"] == "morphir-python" + and initialized["extension"]["version"] == native.compiler_version + ): + raise bundle.BundleError("diagnostic guest identity differs") + cases: dict[str, Any] = {} + for reply, version in zip(replies[1:3], IrVersion, strict=True): + value = reply["result"] + diagnostics = [ + {key: item.get(key) for key in ("code", "severity", "message", "location")} + for item in value["diagnostics"] + ] + if ( + value["success"] is not False + or value.get("ir") is not None + or not diagnostics + or diagnostics != expected[version.value] + ): + raise bundle.BundleError("process and Python API diagnostics differ") + cases[version.value] = { + "diagnostics": diagnostics, + "pythonApi": expected[version.value], + } + if bundle.sha256(executable.read_bytes()) != digest: + raise bundle.BundleError("diagnostic artifact changed during execution") + return { + "scope": "legacy-process-diagnostic-parity", + "artifactSha256": digest, + "checkerSha256": bundle.sha256(Path(__file__).read_bytes()), + "inputSha256": bundle.sha256(SOURCE.encode()), + "compilerVersion": native.compiler_version, + "nativeVersion": native.version, + "protocolVersion": "0.1", + "cleanEnvironment": True, + "exitCode": result.returncode, + "stderr": result.stderr.decode(), + "cases": cases, + } diff --git a/scripts/process_extension.py b/scripts/process_extension.py new file mode 100644 index 00000000..5af1ef6b --- /dev/null +++ b/scripts/process_extension.py @@ -0,0 +1,183 @@ +"""Stage process extensions with the pinned Morphir CLI's canonical bundler. + +Receipts live outside the canonical three-file bundle. They bind its source, +platform, launch convention, executable mode and exact bytes. Publication and +installed-host qualification are separate steps. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + +PLATFORMS = ( + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", +) + + +def artifact_name(version: str, platform: str) -> str: + """Return the raw executable name for an explicitly supported platform.""" + if platform not in PLATFORMS: + raise bundle.BundleError(f"unsupported process platform {platform}") + suffix = ".exe" if platform.endswith("windows-msvc") else "" + return f"morphir-python-extension-{version}-{platform}{suffix}" + + +def receipt(directory: Path, commit: str, platform: str) -> dict[str, Any]: + """Refuse a candidate whose descriptor, bytes, identity or mode disagree.""" + if re.fullmatch(r"[0-9a-f]{40}", commit) is None: + raise bundle.BundleError("source commit must be a full lowercase Git SHA") + version = bundle.crate_version() + name = artifact_name(version, platform) + expected = {name, f"{name}.sha256", "release.json"} + if {p.name for p in directory.iterdir()} != expected: + raise bundle.BundleError("process bundle has missing or extra files") + if any(p.is_symlink() or not p.is_file() for p in directory.iterdir()): + raise bundle.BundleError("process bundle files must be regular files") + try: + descriptor = json.loads((directory / "release.json").read_text()) + (artifact,) = descriptor["artifacts"] + claims = artifact["claims"] + digest = bundle.sha256((directory / name).read_bytes()) + if not ( + descriptor["schemaVersion"] == "2.0.0-draft.2" + and descriptor["extensionId"] == "morphir-python" + and descriptor["shortId"] == "python" + and descriptor["version"] == version + and descriptor["gitCommit"] == commit + and artifact["runtime"] == "process" + and artifact["platform"] == platform + and artifact["filename"] == name + and artifact["sha256"] == digest + and claims["extension"]["id"] == descriptor["extensionId"] + and claims["extension"]["version"] == version + and claims["protocolVersions"] + == bundle.declaration()["extension"]["mep_versions"] + ): + raise bundle.BundleError("process descriptor disagrees with candidate") + if (directory / f"{name}.sha256").read_text() != f"{digest} {name}\n": + raise bundle.BundleError("process checksum disagrees with executable") + except (KeyError, ValueError, TypeError) as error: + raise bundle.BundleError(f"invalid process descriptor: {error}") from error + mode = stat.S_IMODE((directory / name).stat().st_mode) + if os.name != "nt" and mode & stat.S_IXUSR == 0: + raise bundle.BundleError("process artifact lacks owner executable mode") + return { + "schemaVersion": 1, + "sourceCommit": commit, + "runtime": "process", + "platform": platform, + "version": version, + "arguments": [], + "explicitArguments": ["--stdio"], + "executable": True, + "mode": oct(mode), + "files": { + name: bundle.sha256((directory / name).read_bytes()) + for name in sorted(expected) + }, + "claims": claims, + } + + +def package(executable: Path, output: Path, commit: str) -> dict[str, Any]: + """Run the pinned native bundler on a uniquely named copy of exact bytes.""" + platform = bundle._target() + name = artifact_name(bundle.crate_version(), platform) + if re.fullmatch(r"[0-9a-f]{40}", commit) is None: + raise bundle.BundleError("source commit must be a full lowercase Git SHA") + if output.exists() or output.is_symlink(): + raise bundle.BundleError(f"bundle directory {output} already exists") + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("process executable must be a regular file") + cli, archive_digest = bundle.morphir_cli() + with tempfile.TemporaryDirectory(prefix="python-process-bundle-") as temporary: + copied = Path(temporary) / name + shutil.copyfile(executable, copied) + copied.chmod(0o700) + subprocess.run( + [ + str(cli.resolve()), + "--no-banner", + "extension", + "bundle", + "--executable", + str(copied), + "--extension-id", + "morphir-python", + "--short-id", + "python", + "--version", + bundle.crate_version(), + "--platform", + platform, + "--output", + str(output.resolve()), + ], + check=True, + ) + # gitCommit is a canonical optional descriptor member. All runtime fields + # and claims come from the upstream writer and its live describe probe. + descriptor_path = output / "release.json" + descriptor = json.loads(descriptor_path.read_text()) + descriptor["gitCommit"] = commit + descriptor_path.write_text(json.dumps(descriptor, indent=2) + "\n") + result = receipt(output, commit, platform) + result["bundler"] = { + "version": bundle.declaration()["host"]["version"], + "archiveSha256": archive_digest, + "binarySha256": bundle.sha256(cli.read_bytes()), + } + result["cargoLockSha256"] = bundle.sha256( + (bundle.ROOT / "rust/Cargo.lock").read_bytes() + ) + return result + + +def main(argv: list[str] | None = None) -> int: + """Write a source-bound process receipt outside the canonical bundle.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument( + "--host-check", + action="store_true", + help="install offline and compare v3/v4 with the matching Python API", + ) + args = parser.parse_args(argv) + if args.report.resolve().is_relative_to(args.output.resolve()): + parser.error("the report must be outside the canonical bundle") + try: + result = package(args.executable, args.output, args.commit) + if args.host_check: + result["installedHost"] = bundle.host_round_trips(args.output, result) + from scripts.process_diagnostics import qualify + + result["processDiagnostics"] = qualify( + args.output / artifact_name(bundle.crate_version(), bundle._target()) + ) + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + except (bundle.BundleError, OSError, subprocess.CalledProcessError) as error: + parser.exit(1, f"process bundle refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/process_release.py b/scripts/process_release.py new file mode 100644 index 00000000..b79d4c2f --- /dev/null +++ b/scripts/process_release.py @@ -0,0 +1,402 @@ +"""Assemble verified native platforms into a canonical process-only bundle. + +This is artifact assembly, not MEP conformance or publication approval. Every +platform must have an exact-byte installed-host receipt matching the WASM +candidate's claims and supported legacy results. The release promotion gate +must additionally require independent executable MEP qualification. +""" + +from __future__ import annotations + +import argparse +import gzip +import io +import json +import re +import shutil +import tarfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle +from scripts import process_diagnostics, qualify_mep +from scripts import process_extension as process +from scripts.qualify_native import tools_pin +from scripts.qualify_runtime import _descriptor +from scripts.release_candidates import CandidateError, bundle_candidates + + +def _read(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text()) + if not isinstance(value, dict): + raise bundle.BundleError(f"expected an object in {path}") + return value + + +def _windows_imports(root: Path, artifact_digest: str, commit: str) -> str: + path = root / ".dev/qualification/windows-process-imports.json" + proof = _read(path) + try: + dependencies = proof["dependencies"] + if not ( + proof["sourceCommit"] == commit + and proof["crt"] == "static" + and proof["artifact"]["sha256"] == artifact_digest + and proof["checkerSha256"] + == bundle.sha256( + (bundle.ROOT / ".github/scripts/check_process_windows.ps1").read_bytes() + ) + and proof["tool"]["name"] == "dumpbin.exe" + and re.fullmatch(r"[0-9a-f]{64}", proof["tool"]["sha256"]) + and proof["tool"]["version"] + and isinstance(dependencies, list) + and dependencies + and all( + isinstance(name, str) and re.fullmatch(r"(?i)[a-z0-9_.-]+\.dll", name) + for name in dependencies + ) + and not any( + re.match(r"(?i)(vcruntime|msvcp|concrt|python)\d", name) + for name in dependencies + ) + ): + raise bundle.BundleError("Windows process runtime dependency proof differs") + except (KeyError, TypeError) as error: + raise bundle.BundleError("incomplete Windows process runtime proof") from error + return bundle.sha256(path.read_bytes()) + + +def _diagnostics(proof: dict[str, Any], digest: str, version: str) -> None: + if not ( + proof["scope"] == "legacy-process-diagnostic-parity" + and proof["artifactSha256"] == digest + and proof["checkerSha256"] + == bundle.sha256(Path(process_diagnostics.__file__).read_bytes()) + and proof["inputSha256"] == bundle.sha256(process_diagnostics.SOURCE.encode()) + and proof["compilerVersion"] == version + and proof["nativeVersion"] == tools_pin() + and proof["protocolVersion"] == "0.1" + and proof["cleanEnvironment"] is True + and proof["exitCode"] == 0 + and proof["stderr"] == "" + and isinstance(proof["cases"], dict) + and set(proof["cases"]) == {"3", "4"} + ): + raise bundle.BundleError("process diagnostic proof identity differs") + for case in proof["cases"].values(): + diagnostics = case["diagnostics"] + if not ( + diagnostics == case["pythonApi"] + and len(diagnostics) == 1 + and diagnostics[0]["code"] == "PY004" + and diagnostics[0]["severity"] == "error" + ): + raise bundle.BundleError("process diagnostics differ from the Python API") + + +def assemble( + inputs: Path, + output: Path, + commit: str, + wasm_claims: dict[str, Any], + reference_results: dict[str, Any], +) -> dict[str, Any]: + """Refuse mixed candidates and combine all canonical process entries.""" + if output.exists() or output.is_symlink(): + raise FileExistsError(output) + expected = {f"morphir-python-process-{target}" for target in process.PLATFORMS} + if {path.name for path in inputs.iterdir()} != expected: + raise bundle.BundleError("process set must contain exactly five platforms") + if set(reference_results) != {"v3", "v4"} or not all( + result.get("agrees_with_python_api") is True and result.get("modules") + for result in reference_results.values() + ): + raise bundle.BundleError("WASM reference lacks successful v3/v4 results") + version = bundle.crate_version() + lock_digest = bundle.sha256((bundle.ROOT / "rust/Cargo.lock").read_bytes()) + source_files: list[Path] = [] + artifacts: list[dict[str, Any]] = [] + reports: dict[str, str] = {} + windows_imports_digest = "" + for target in process.PLATFORMS: + root = inputs / f"morphir-python-process-{target}" + directory = root / ".morphir/build/extensions/python-process" + report_path = root / f".dev/qualification/process-{target}.json" + report = _read(report_path) + name = process.artifact_name(version, target) + executable = directory / name + # GitHub artifact ZIP transfer strips execute bits. Restore only the + # declared raw executable; its bytes and original mode remain checked. + if executable.is_symlink() or not executable.is_file(): + raise bundle.BundleError("process artifact is not a regular file") + executable.chmod(0o700) + checked = process.receipt(directory, commit, target) + if target.endswith("windows-msvc"): + windows_imports_digest = _windows_imports( + root, checked["files"][name], commit + ) + try: + _diagnostics(report["processDiagnostics"], checked["files"][name], version) + for field in ( + "sourceCommit", + "runtime", + "platform", + "version", + "files", + "arguments", + "explicitArguments", + "executable", + "claims", + ): + if report[field] != checked[field]: + raise bundle.BundleError(f"{target}: mismatched {field}") + host = report["installedHost"] + pin = bundle.declaration()["host"] + if not ( + report["claims"] == wasm_claims + and report["cargoLockSha256"] == lock_digest + and report["bundler"]["version"] == pin["version"] + and report["bundler"]["archiveSha256"] == pin["sha256"][target] + and host["sourceCommit"] == commit + and host["version"] == version + and host["files"] == checked["files"] + and host["installed_integrity"]["expected_sha256"] + == checked["files"][name] + and re.fullmatch( + r"[0-9a-f]{64}", host["installed_integrity"]["tampered_sha256"] + ) + and host["installed_integrity"]["tampered_sha256"] + != checked["files"][name] + and host["installed_integrity"]["exit_code"] != 0 + and host["installed_integrity"]["fresh_project"] is True + and bundle.is_digest_mismatch( + host["installed_integrity"]["stdout"] + + host["installed_integrity"]["stderr"], + checked["files"][name], + host["installed_integrity"]["tampered_sha256"], + ) + and host["host"]["morphir"] == pin["version"] + and host["host"]["sha256"] == pin["sha256"][target] + and host["host"]["binary_sha256"] == report["bundler"]["binarySha256"] + and host["qualification_script_sha256"] + == bundle.sha256( + (bundle.ROOT / "scripts/extension_bundle.py").read_bytes() + ) + and host["python_api"]["native_version"] == tools_pin() + and host["python_api"]["compiler_version"] == version + and host["offline_round_trips"] == reference_results + and (target.endswith("windows-msvc") or report["mode"] == "0o700") + ): + raise bundle.BundleError(f"{target}: qualification evidence differs") + descriptor = _read(directory / "release.json") + artifacts.append(descriptor["artifacts"][0]) + except (KeyError, TypeError, IndexError) as error: + raise bundle.BundleError( + f"{target}: incomplete process evidence" + ) from error + source_files.extend([executable, directory / f"{name}.sha256"]) + reports[target] = bundle.sha256(report_path.read_bytes()) + descriptor = { + "schemaVersion": "2.0.0-draft.2", + "extensionId": "morphir-python", + "shortId": "python", + "version": version, + "gitCommit": commit, + "platformDifferences": "none", + "artifacts": artifacts, + } + # mkdir is exclusive; a concurrent or existing destination is never replaced. + output.mkdir() + try: + for path in source_files: + shutil.copyfile(path, output / path.name) + if not path.name.endswith(".sha256"): + (output / path.name).chmod(0o700) + (output / "release.json").write_text(json.dumps(descriptor, indent=2) + "\n") + except BaseException: + shutil.rmtree(output) + raise + return { + "kind": "process-release-assembly", + "sourceCommit": commit, + "version": version, + "platforms": list(process.PLATFORMS), + "files": { + path.name: bundle.sha256(path.read_bytes()) + for path in sorted(output.iterdir()) + }, + "inputReports": reports, + "windowsImportsSha256": windows_imports_digest, + } + + +def archive_bundle( + directory: Path, assembly: Path, wasm: Path, output: Path, commit: str +) -> dict[str, Any]: + """Seal a portable download from verified candidate bytes, without publishing. + + This packaging receipt does not replace host assurance or the independent + executable MCK promotion gate. The current workflow retains candidates only. + """ + version = bundle.crate_version() + expected = {"release.json"} + for target in process.PLATFORMS: + name = process.artifact_name(version, target) + expected.update((name, f"{name}.sha256")) + if {path.name for path in directory.iterdir()} != expected or any( + path.is_symlink() or not path.is_file() for path in directory.iterdir() + ): + raise bundle.BundleError("archive input is not the exact regular process set") + descriptor = _descriptor(directory, "process") + reference = _descriptor(wasm, "wasm") + receipt = _read(assembly) + files = { + name: bundle.sha256((directory / name).read_bytes()) + for name in sorted(expected) + } + if not ( + receipt["kind"] == "process-release-assembly" + and receipt["sourceCommit"] == commit + and receipt["version"] == version + and receipt["platforms"] == list(process.PLATFORMS) + and receipt["files"] == files + and descriptor["extensionId"] == reference["extensionId"] == "morphir-python" + and descriptor["version"] == reference["version"] == version + and descriptor["gitCommit"] == reference["gitCommit"] == commit + and len(reference["artifacts"]) == 1 + and len(descriptor["artifacts"]) == len(process.PLATFORMS) + and {entry["platform"] for entry in descriptor["artifacts"]} + == set(process.PLATFORMS) + and all( + entry["filename"] == process.artifact_name(version, entry["platform"]) + and entry["claims"] == reference["artifacts"][0]["claims"] + for entry in descriptor["artifacts"] + ) + ): + raise bundle.BundleError("archive source, bytes or runtime claims differ") + if output.exists() or output.is_symlink(): + raise FileExistsError(output) + if output.resolve().is_relative_to( + directory.resolve() + ) or output.resolve().is_relative_to(wasm.resolve()): + raise bundle.BundleError("archive assets must remain outside canonical bundles") + output.mkdir() + try: + descriptor_name = f"morphir-python-extension-{version}.release.json" + for name in sorted(expected): + destination = output / (descriptor_name if name == "release.json" else name) + shutil.copyfile(directory / name, destination) + if bundle.sha256(destination.read_bytes()) != files[name]: + raise bundle.BundleError("archive asset transfer changed bytes") + destination.chmod(0o644 if name.endswith((".json", ".sha256")) else 0o700) + archive_path = output / f"morphir-python-extension-{version}.tar.gz" + # Normalize only the container metadata; preserve each original payload. + with ( + archive_path.open("xb") as raw, + gzip.GzipFile(filename="", fileobj=raw, mode="wb", mtime=0) as compressed, + tarfile.open(fileobj=compressed, mode="w") as archive, + ): + for name in sorted(expected): + data = (directory / name).read_bytes() + if bundle.sha256(data) != files[name]: + raise bundle.BundleError("archive payload changed during packaging") + info = tarfile.TarInfo(name) + info.size = len(data) + info.mode = 0o644 if name.endswith((".json", ".sha256")) else 0o700 + archive.addfile(info, io.BytesIO(data)) + result = { + "kind": "process-download-assets", + "sourceCommit": commit, + "version": version, + "assemblySha256": bundle.sha256(assembly.read_bytes()), + "bundleFiles": files, + "files": { + path.name: bundle.sha256(path.read_bytes()) + for path in sorted(output.iterdir()) + }, + } + (output / "process-assets.json").write_text(json.dumps(result, indent=2) + "\n") + (output / "SHA256SUMS").write_text( + "".join( + f"{bundle.sha256(path.read_bytes())} {path.name}\n" + for path in sorted(output.iterdir()) + ) + ) + except BaseException: + shutil.rmtree(output) + raise + return result + + +def main(argv: list[str] | None = None) -> int: + """Assemble a process candidate against source-bound WASM reference bytes.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--inputs", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--wasm-bundle", type=Path, required=True) + parser.add_argument("--wasm-report", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--assets-output", type=Path) + args = parser.parse_args(argv) + if args.report.resolve().is_relative_to(args.output.resolve()): + parser.error("assembly report must remain outside the canonical bundle") + try: + bundle_candidates(args.wasm_bundle, bundle.crate_version(), args.commit) + checked = bundle.receipt(args.wasm_bundle) + reference = _read(args.wasm_report) + if not ( + reference["git_commit"] == args.commit + and reference["bundle"] == checked["bundle"] + and reference["installed_integrity"]["expected_sha256"] + == checked["wasm_sha256"] + and reference["installed_integrity"]["exit_code"] != 0 + and reference["installed_integrity"]["fresh_project"] is True + and bundle.is_digest_mismatch( + reference["installed_integrity"]["stdout"] + + reference["installed_integrity"]["stderr"], + str(checked["wasm_sha256"]), + reference["installed_integrity"]["tampered_sha256"], + ) + and reference["qualification_script_sha256"] + == bundle.sha256((bundle.ROOT / "scripts/extension_bundle.py").read_bytes()) + and reference["python_api"]["native_version"] == tools_pin() + and reference["python_api"]["compiler_version"] == bundle.crate_version() + ): + raise bundle.BundleError("WASM reference is not this qualified candidate") + claims = _read(args.wasm_bundle / "release.json")["artifacts"][0]["claims"] + canonical_mep = qualify_mep.verify_release( + args.inputs, args.wasm_bundle, args.wasm_report.parent, args.commit + ) + result = assemble( + args.inputs, + args.output, + args.commit, + claims, + reference["offline_round_trips"], + ) + result["canonicalMep"] = canonical_mep + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + if args.assets_output is not None: + archive_bundle( + args.output, + args.report, + args.wasm_bundle, + args.assets_output, + args.commit, + ) + except ( + bundle.BundleError, + CandidateError, + OSError, + KeyError, + ValueError, + TypeError, + ) as error: + parser.exit(1, f"process release assembly refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/qualify_mep.py b/scripts/qualify_mep.py new file mode 100644 index 00000000..2d9e50cc --- /dev/null +++ b/scripts/qualify_mep.py @@ -0,0 +1,495 @@ +"""Run canonical MEP qualification and bind its producer recheck for transfer. + +Only the shared executable MCK adjudicates observations. This module verifies +tool/artifact identities and the trusted same-workflow producer handoff; it does +not replace canonical report-check or rewrite producer filesystem identities. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import ntpath +import os +import posixpath +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle +from scripts import mep_tool +from scripts.qualify_runtime import _descriptor + + +def argument_blob(value: str, *, windows: bool) -> dict[str, list[int]]: + """Match the canonical runner's lossless serde OsString representation.""" + if not windows: + return {"Unix": list(os.fsencode(value))} + encoded = value.encode("utf-16-le", errors="surrogatepass") + return { + "Windows": [ + int.from_bytes(encoded[index : index + 2], "little") + for index in range(0, len(encoded), 2) + ] + } + + +def _same_recorded_path(left: str, right: str, *, windows: bool) -> bool: + # Rust canonicalization adds the Windows extended-length prefix. Never + # resolve another producer's paths against the collector's filesystem. + if windows: + + def normalize(value: str) -> str: + value = value.replace("/", "\\") + if value.startswith("\\\\?\\UNC\\"): + value = "\\\\" + value[8:] + elif value.startswith("\\\\?\\"): + value = value[4:] + return ntpath.normcase(ntpath.normpath(value)) + + return normalize(left) == normalize(right) + return posixpath.normpath(left) == posixpath.normpath(right) + + +def _bind_roles( + roles: dict[str, dict[str, str]], identity: dict[str, Any] +) -> dict[str, dict[str, str]]: + """Bind live producer files to the runner's own canonical identities.""" + bound = {} + for name, expected in roles.items(): + matches = [ + record + for record in identity["files"] + if record["sha256"] == expected["sha256"] + and os.path.samefile(record["path"], expected["path"]) + ] + if len(matches) != 1: + raise bundle.BundleError(f"MCK file identity differs for {name}") + bound[name] = matches[0] + return bound + + +def definition_hashes() -> dict[str, str]: + """Identify the exact producer definition and pin configuration.""" + return { + name: mep_tool.sha(bundle.ROOT / name) + for name in ( + "scripts/qualify_mep.py", + "scripts/mep_tool.py", + "rust/extension.toml", + ) + } + + +def case_set_sha(records: list[dict[str, Any]]) -> str: + """Hash the complete required identity set, independently of record order.""" + identifiers = [record["caseId"] for record in records] + if any(not isinstance(value, str) for value in identifiers) or len( + set(identifiers) + ) != len(identifiers): + raise bundle.BundleError("MCK observation identities are invalid or duplicated") + return hashlib.sha256( + json.dumps(sorted(identifiers), separators=(",", ":")).encode() + ).hexdigest() + + +def _tool(path: Path) -> dict[str, Any]: + tool = mep_tool.object_file(path) + pin = bundle.declaration()["mep"] + if not ( + tool["kind"] == "source-built-shared-mck" + and tool["revision"] == pin["rev"] + and tool["tree"] == pin["tree"] + and tool["corpusSha256"] == pin["corpus_sha256"] + and tool["cargoLockSha256"] == pin["cargo_lock_sha256"] + and tool["preparerSha256"] == mep_tool.sha(Path(mep_tool.__file__)) + and mep_tool.sha(Path(tool["executable"])) == tool["binarySha256"] + and mep_tool.sha(Path(tool["kit"])) == pin["corpus_sha256"] + ): + raise bundle.BundleError("MCK tool receipt or available bytes differ") + return tool + + +def _verify_transfer( + receipt: dict[str, Any], + report_path: Path, + *, + commit: str, + runtime: str, + platform: str, + artifact_sha: str, + claims: dict[str, Any], +) -> dict[str, Any]: + """Verify exact evidence from successful producers in this workflow run. + + Canonical report-check already ran on the producer with the original + checker and identity paths. Those paths can be Windows/macOS-specific; + they are never rewritten or falsely rechecked on the Linux collector. + Workflow needs, source/checker definitions, complete records and byte-bound + receipts form the explicit transfer contract. This accepts no standalone + 'qualified' boolean as conformance evidence. + """ + pin = bundle.declaration()["mep"] + report = mep_tool.object_file(report_path) + identity = report["identity"] + invocation = receipt["invocation"] + windows = invocation["windows"] + roles = receipt["roles"] + expected_roles = {"guest", "compilerLock"} + if runtime == "wasm": + expected_roles |= {"adapter", "adapterLock", "adapterSource"} + if not ( + receipt["kind"] == "canonical-mep-producer-recheck" + and receipt["sourceCommit"] == commit + and receipt["runtime"] == runtime + and receipt["platform"] == platform + and receipt["artifactSha256"] == artifact_sha + and receipt["definitionInputs"] == definition_hashes() + and receipt["reportSha256"] == mep_tool.sha(report_path) + and receipt["reportFilename"] == report_path.name + and receipt["recheck"]["exitCode"] == 0 + and receipt["recheck"]["stdout"].strip() == "MEP guest report: qualified" + and receipt["recheck"]["stderr"] == "" + and report["contractVersion"] == "0.1.0-draft.1" + and report["suite"] == "mep-guest" + and report["profile"] == pin["profile"] + and report["corpusSha256"] == pin["corpus_sha256"] + and report["qualified"] is True + and report["error"] is None + and len(report["records"]) == pin["required_cases"] + and case_set_sha(report["records"]) == pin["case_set_sha256"] + and identity["runner"] + == { + "name": "morphir", + "version": pin["runner_version"], + "commit": pin["rev"], + "dirty": False, + } + and identity["runnerBinary"]["sha256"] == receipt["checker"]["binarySha256"] + and receipt["checker"]["revision"] == pin["rev"] + and receipt["checker"]["tree"] == pin["tree"] + and receipt["checker"]["cargoLockSha256"] == pin["cargo_lock_sha256"] + and identity == receipt["checkedIdentity"] + and set(roles) == expected_roles + and len(identity["files"]) == len(roles) + and all(role in identity["files"] for role in roles.values()) + and len({role["path"] for role in roles.values()}) == len(roles) + and isinstance(windows, bool) + and windows == platform.endswith("windows-msvc") + and identity["arguments"] + == [argument_blob(value, windows=windows) for value in invocation["arguments"]] + and _same_recorded_path( + invocation["host"], identity["host"]["path"], windows=windows + ) + and _same_recorded_path( + receipt["checker"]["executable"], + identity["runnerBinary"]["path"], + windows=windows, + ) + and receipt["roles"]["guest"] in identity["files"] + and receipt["roles"]["guest"]["sha256"] == artifact_sha + and receipt["roles"]["compilerLock"] in identity["files"] + and receipt["roles"]["compilerLock"]["sha256"] + == mep_tool.sha(bundle.ROOT / "rust/Cargo.lock") + ): + raise bundle.BundleError( + "MCK producer transfer is missing, stale or mismatched" + ) + describes = [ + record for record in report["records"] if record["caseId"] == "describe" + ] + if len(describes) != 1 or describes[0]["observed"]["result"] != claims: + raise bundle.BundleError("MCK discovery differs from staged artifact claims") + if runtime == "process": + if identity["host"] != receipt["roles"]["guest"] or identity["arguments"]: + raise bundle.BundleError( + "MCK process host is not the actual bare executable" + ) + elif runtime == "wasm": + if not ( + len(identity["arguments"]) == 1 + and _same_recorded_path( + invocation["arguments"][0], roles["guest"]["path"], windows=windows + ) + and identity["host"] == receipt["roles"]["adapter"] + and receipt["roles"]["adapterLock"] in identity["files"] + and receipt["roles"]["adapterLock"]["sha256"] + == mep_tool.sha(bundle.ROOT / "tools/extension-qualification/Cargo.lock") + and receipt["roles"]["adapterSource"] in identity["files"] + and receipt["roles"]["adapterSource"]["sha256"] + == mep_tool.sha( + bundle.ROOT / "tools/extension-qualification/src/bin/mep_wasm_host.rs" + ) + ): + raise bundle.BundleError("MCK WASM adapter identity differs") + else: + raise bundle.BundleError("unsupported MCK runtime") + return report + + +def verify_transfer( + receipt: dict[str, Any], + report_path: Path, + *, + commit: str, + runtime: str, + platform: str, + artifact_sha: str, + claims: dict[str, Any], +) -> dict[str, Any]: + """Reject incomplete evidence as well as mismatched evidence.""" + try: + return _verify_transfer( + receipt, + report_path, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=artifact_sha, + claims=claims, + ) + except (KeyError, TypeError, ValueError, IndexError, OSError) as error: + raise bundle.BundleError( + "incomplete canonical MCK producer evidence" + ) from error + + +def qualify( + directory: Path, + runtime: str, + tool_path: Path, + output: Path, + receipt_path: Path, + commit: str, + adapter: Path | None = None, +) -> dict[str, Any]: + """Execute the canonical suite and saved recheck while all identities exist.""" + directory = directory.resolve() + if output.resolve().is_relative_to( + directory + ) or receipt_path.resolve().is_relative_to(directory): + raise bundle.BundleError("MCK evidence must remain outside canonical bundles") + descriptor = _descriptor(directory, runtime) + if ( + descriptor["gitCommit"] != commit + or descriptor["version"] != bundle.crate_version() + ): + raise bundle.BundleError("MCK candidate source/version differs") + platform = bundle._target() if runtime == "process" else "portable" + selected = [ + entry + for entry in descriptor["artifacts"] + if runtime == "wasm" or entry["platform"] == platform + ] + if len(selected) != 1: + raise bundle.BundleError("MCK needs exactly the selected runtime artifact") + entry = selected[0] + guest = (directory / entry["filename"]).resolve() + roles: dict[str, dict[str, str]] = { + "guest": {"path": str(guest), "sha256": mep_tool.sha(guest)}, + "compilerLock": { + "path": str(bundle.ROOT / "rust/Cargo.lock"), + "sha256": mep_tool.sha(bundle.ROOT / "rust/Cargo.lock"), + }, + } + host, arguments = guest, [] + if runtime == "wasm": + if adapter is None: + raise bundle.BundleError( + "WASM MCK qualification needs its concrete adapter" + ) + host = adapter.resolve(strict=True) + arguments = [str(guest)] + for name, path in ( + ("adapter", host), + ("adapterLock", bundle.ROOT / "tools/extension-qualification/Cargo.lock"), + ( + "adapterSource", + bundle.ROOT / "tools/extension-qualification/src/bin/mep_wasm_host.rs", + ), + ): + roles[name] = {"path": str(path), "sha256": mep_tool.sha(path)} + tool = _tool(tool_path) + output = output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + command = [ + tool["executable"], + "--no-banner", + "mck", + "mep", + "guest-run", + "--kit", + tool["kit"], + "--expect-kit-sha256", + tool["corpusSha256"], + "--host", + str(host), + "--report", + str(output), + ] + for argument in arguments: + command.extend(("--host-arg", argument)) + for role in roles.values(): + command.extend(("--identity-file", role["path"])) + with tempfile.TemporaryDirectory(prefix="canonical-python-mep-") as scratch: + environment = os.environ | {"MORPHIR_HOME": scratch, "MORPHIR_NO_BANNER": "1"} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + environment[name] = environment[name.lower()] = bundle.OFFLINE + environment.pop("NO_PROXY", None) + environment.pop("no_proxy", None) + first = subprocess.run( + command, env=environment, capture_output=True, text=True, timeout=300 + ) + if first.returncode: + raise bundle.BundleError( + f"canonical MCK run failed: {first.stdout} {first.stderr}; " + f"report {output}" + ) + recheck_command = [ + tool["executable"], + "--no-banner", + "mck", + "mep", + "report-check", + "--kit", + tool["kit"], + str(output), + ] + recheck = subprocess.run( + recheck_command, env=environment, capture_output=True, text=True, timeout=60 + ) + if recheck.returncode: + raise bundle.BundleError( + f"canonical MCK recheck failed: {recheck.stdout} {recheck.stderr}" + ) + report = mep_tool.object_file(output) + if report["identity"]["arguments"] != [ + argument_blob(value, windows=os.name == "nt") for value in arguments + ]: + raise bundle.BundleError("MCK invocation arguments differ") + roles = _bind_roles(roles, report["identity"]) + receipt = { + "kind": "canonical-mep-producer-recheck", + "sourceCommit": commit, + "runtime": runtime, + "platform": platform, + "artifactSha256": entry["sha256"], + "definitionInputs": definition_hashes(), + "checker": tool, + "reportFilename": output.name, + "reportSha256": mep_tool.sha(output), + "roles": roles, + "invocation": { + "host": str(host), + "arguments": arguments, + "windows": os.name == "nt", + }, + "checkedIdentity": report["identity"], + "recheck": { + "exitCode": recheck.returncode, + "stdout": recheck.stdout, + "stderr": recheck.stderr, + }, + } + verify_transfer( + receipt, + output, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=entry["sha256"], + claims=entry["claims"], + ) + receipt_path.parent.mkdir(parents=True, exist_ok=True) + receipt_path.write_text(json.dumps(receipt, indent=2) + "\n") + return receipt + + +def verify_release( + inputs: Path, wasm_bundle: Path, wasm_proofs: Path, commit: str +) -> dict[str, Any]: + """Require all six producer rechecks before the existing release assembly.""" + from scripts.process_extension import PLATFORMS + + proofs = [] + for target in PLATFORMS: + root = inputs / f"morphir-python-process-{target}" + proofs.append( + ( + root / ".morphir/build/extensions/python-process", + root / ".dev/qualification", + "process", + target, + f"mep-{target}", + ) + ) + proofs.append((wasm_bundle, wasm_proofs, "wasm", "portable", "bundle-mep")) + results = [] + for directory, evidence, runtime, platform, prefix in proofs: + descriptor = _descriptor(directory, runtime) + if descriptor["gitCommit"] != commit: + raise bundle.BundleError("MCK release source differs") + (entry,) = descriptor["artifacts"] + if runtime == "process" and entry["platform"] != platform: + raise bundle.BundleError("MCK release platform differs") + report_path = evidence / f"{prefix}-report.json" + receipt_path = evidence / f"{prefix}-recheck.json" + receipt = mep_tool.object_file(receipt_path) + verify_transfer( + receipt, + report_path, + commit=commit, + runtime=runtime, + platform=platform, + artifact_sha=mep_tool.sha(directory / entry["filename"]), + claims=entry["claims"], + ) + results.append( + { + "runtime": runtime, + "platform": platform, + "artifactSha256": entry["sha256"], + "reportFilename": report_path.name, + "reportSha256": mep_tool.sha(report_path), + "receiptFilename": receipt_path.name, + "receiptSha256": mep_tool.sha(receipt_path), + "checkerBinarySha256": receipt["checker"]["binarySha256"], + } + ) + pin = bundle.declaration()["mep"] + return { + "sourceCommit": commit, + "profile": pin["profile"], + "checkerRevision": pin["rev"], + "corpusSha256": pin["corpus_sha256"], + "requiredCasesPerArtifact": pin["required_cases"], + "producerRechecks": results, + } + + +def main() -> None: + """Qualify one producer's canonical bundle using an explicitly prepared tool.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--bundle", type=Path, required=True) + parser.add_argument("--runtime", choices=("process", "wasm"), required=True) + parser.add_argument("--tool", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--adapter", type=Path) + args = parser.parse_args() + qualify( + args.bundle, + args.runtime, + args.tool, + args.report, + args.receipt, + args.commit, + args.adapter, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/qualify_native.py b/scripts/qualify_native.py index 18f22e10..8e5ef914 100644 --- a/scripts/qualify_native.py +++ b/scripts/qualify_native.py @@ -305,7 +305,14 @@ def _mismatch(python: Path, path: str) -> None: text=True, ).stdout.strip() file = Path(metadata) / "METADATA" - file.write_text(re.sub(r"(?m)^Version: .*$", "Version: 0.0.0", file.read_text())) + # uv may hard-link installed metadata to its wheel cache. Replace this + # installation's directory entry so the negative fixture cannot corrupt + # the cache or another environment's copy of the qualified distribution. + replacement = file.with_name("METADATA.mismatch") + replacement.write_text( + re.sub(r"(?m)^Version: .*$", "Version: 0.0.0", file.read_text()) + ) + replacement.replace(file) result = subprocess.run( [ _script(python, "morphir-python"), diff --git a/scripts/qualify_runtime.py b/scripts/qualify_runtime.py new file mode 100644 index 00000000..80d68524 --- /dev/null +++ b/scripts/qualify_runtime.py @@ -0,0 +1,320 @@ +"""Verify runtime selection and installed-byte integrity through the pinned CLI. + +Use canonical WASM and process bundles from the same candidate. The process +bundle must include the current platform and a real foreign platform. This is +installed legacy-route qualification, independent of executable MEP conformance. +""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from scripts import extension_bundle as bundle + + +def _descriptor(directory: Path, runtime: str) -> dict[str, Any]: + descriptor = json.loads((directory / "release.json").read_text()) + expected = {"release.json"} + for entry in descriptor["artifacts"]: + name = entry["filename"] + if "/" in name or "\\" in name or name in {".", ".."}: + raise bundle.BundleError("bundle artifact filename must be portable") + path = directory / name + if entry["runtime"] != runtime or path.is_symlink() or not path.is_file(): + raise bundle.BundleError("bundle runtime or artifact kind differs") + digest = bundle.sha256(path.read_bytes()) + if entry["sha256"] != digest: + raise bundle.BundleError("bundle artifact digest differs") + if (directory / f"{name}.sha256").read_text() != f"{digest} {name}\n": + raise bundle.BundleError("bundle checksum differs") + expected.update((name, f"{name}.sha256")) + if {path.name for path in directory.iterdir()} != expected: + raise bundle.BundleError("bundle inventory differs") + return dict(descriptor) + + +def qualify(wasm: Path, process: Path) -> dict[str, Any]: + """Exercise actual selected bytes in one home with fresh project caches.""" + wasm, process = wasm.resolve(), process.resolve() + declarations = { + "wasm": _descriptor(wasm, "wasm"), + "process": _descriptor(process, "process"), + } + first, second = declarations.values() + if not ( + first["extensionId"] == second["extensionId"] == "morphir-python" + and first["version"] == second["version"] == bundle.crate_version() + and first["gitCommit"] == second["gitCommit"] + ): + raise bundle.BundleError("runtime bundles are not the same candidate") + platform = bundle._target() + foreign = [entry for entry in second["artifacts"] if entry["platform"] != platform] + if not foreign: + raise bundle.BundleError("platform refusal check needs a real foreign artifact") + cli, archive_digest = bundle.morphir_cli() + cli = cli.resolve() + switches: list[dict[str, Any]] = [] + with tempfile.TemporaryDirectory(prefix="morphir-runtime-choice-") as temporary: + root = Path(temporary) + home, admin = root / "home", root / "admin" + admin.mkdir() + env = {**os.environ, "MORPHIR_HOME": str(home), "MORPHIR_NO_BANNER": "1"} + for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + env[name] = env[name.lower()] = bundle.OFFLINE + env.pop("NO_PROXY", None) + env.pop("no_proxy", None) + + def invoke( + cwd: Path, *arguments: str, fail: bool = False + ) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + [str(cli), "--no-banner", *arguments], + cwd=cwd, + env=env, + capture_output=True, + text=True, + timeout=60, + ) + if (result.returncode != 0) != fail: + raise bundle.BundleError( + f"runtime check {arguments}: {result.stdout} {result.stderr}" + ) + return result + + def project(name: str) -> Path: + directory = root / name + shutil.copytree(bundle.SAMPLE, directory / "src") + (directory / "morphir.toml").write_text( + '[project]\nname = "acme/example"\nversion = "0.1.0"\n' + 'source_directory = "src"\n[frontend]\nlanguage = "python"\n' + "emit_parse_stage = false\n" + ) + return directory + + for runtime, directory in (("wasm", wasm), ("process", process)): + repository = root / f"repository-{runtime}" + name = f"python-{runtime}" + invoke(admin, "extension", "repository", "init", str(repository)) + invoke( + admin, + "extension", + "repository", + "add", + name, + "--directory", + str(repository), + ) + invoke( + admin, + "extension", + "repository", + "publish", + name, + "--bundle", + str(directory), + ) + invoke(admin, "extension", "repository", "verify", name) + + for index, runtime in enumerate(("wasm", "process", "wasm")): + current = project(f"project-{index}") + invoke( + current, + "extension", + "install", + "morphir-python", + "--repository", + f"python-{runtime}", + "--version", + first["version"], + ) + catalog = json.loads((home / "catalog/extensions.json").read_text()) + (installed,) = catalog["extensions"] + entries = declarations[runtime]["artifacts"] + (selected,) = [ + entry + for entry in entries + if runtime == "wasm" or entry["platform"] == platform + ] + if not ( + installed["runtime"] == runtime + and installed["digest"] == selected["sha256"] + and installed["version"] == first["version"] + and installed["args"] == [] + ): + raise bundle.BundleError( + "installed catalog differs from the selected runtime" + ) + stored = home / installed["storePath"] + if bundle.sha256(stored.read_bytes()) != selected["sha256"]: + raise bundle.BundleError( + "installed bytes differ from selected artifact" + ) + results: dict[str, dict[str, object]] = {} + for version in ("3.0.0", "4") if index < 2 else ("4",): + tag = version[0] + invoke( + current, + "compile", + "--language", + "python", + "--input", + "src", + "--ir-version", + version, + "--output", + f"compiled-{tag}", + ) + document = json.loads( + (current / f"compiled-{tag}/morphir-ir.json").read_text() + ) + results[f"v{tag}"] = { + "ir_sha256": bundle.sha256( + json.dumps( + document, sort_keys=True, separators=(",", ":") + ).encode() + ) + } + if index < 2: + generated = current / f"generated-{tag}" + invoke( + current, + "generate", + "--target", + "python", + "--input", + f"compiled-{tag}/morphir-ir.json", + "--output", + str(generated), + ) + results[f"v{tag}"]["generated_sha256"] = { + file.relative_to(generated).as_posix(): bundle.sha256( + file.read_bytes() + ) + for file in sorted(generated.rglob("*.py")) + } + switches.append( + { + "runtime": runtime, + "artifact_sha256": selected["sha256"], + "results": results, + } + ) + if index == 1 and switches[0]["results"] != results: + raise bundle.BundleError("WASM and process installed results differ") + if index == 2: + if ( + results["v4"]["ir_sha256"] + != switches[0]["results"]["v4"]["ir_sha256"] + ): + raise bundle.BundleError( + "switching back to WASM changed the result" + ) + integrity = bundle.check_installed_integrity(cli, home, env) + invoke(current, "extension", "uninstall", "morphir-python") + + # Publish only real non-current platform bytes into a separate local index. + foreign_bundle = root / "foreign-bundle" + foreign_bundle.mkdir() + descriptor = copy.deepcopy(second) + descriptor["artifacts"] = foreign + for entry in foreign: + for name in (entry["filename"], f"{entry['filename']}.sha256"): + shutil.copyfile(process / name, foreign_bundle / name) + (foreign_bundle / "release.json").write_text( + json.dumps(descriptor, indent=2) + "\n" + ) + foreign_repo = root / "repository-foreign" + invoke(admin, "extension", "repository", "init", str(foreign_repo)) + invoke( + admin, + "extension", + "repository", + "add", + "python-foreign", + "--directory", + str(foreign_repo), + ) + invoke( + admin, + "extension", + "repository", + "publish", + "python-foreign", + "--bundle", + str(foreign_bundle), + ) + rejected = invoke( + admin, + "extension", + "install", + "morphir-python", + "--repository", + "python-foreign", + "--version", + first["version"], + fail=True, + ) + if "platform" not in (rejected.stdout + rejected.stderr).lower(): + raise bundle.BundleError("foreign-only bundle was not refused for platform") + final_catalog = json.loads((home / "catalog/extensions.json").read_text()) + if final_catalog["extensions"]: + raise bundle.BundleError("unsupported platform unexpectedly installed") + return { + "scope": "installed-legacy-runtime-selection", + "sourceCommit": first["gitCommit"], + "version": first["version"], + "host": { + "version": bundle.declaration()["host"]["version"], + "archive_sha256": archive_digest, + "binary_sha256": bundle.sha256(cli.read_bytes()), + }, + "qualification_script_sha256": bundle.sha256(Path(__file__).read_bytes()), + "descriptors": { + runtime: bundle.sha256((directory / "release.json").read_bytes()) + for runtime, directory in (("wasm", wasm), ("process", process)) + }, + "integrity_checker_sha256": bundle.sha256(Path(bundle.__file__).read_bytes()), + "installed_integrity": integrity, + "switches": switches, + "tampered_install_refused": True, + "unsupported_platform_refused": True, + } + + +def main(argv: list[str] | None = None) -> int: + """Write actual runtime-choice evidence outside the supplied bundles.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--wasm", type=Path, required=True) + parser.add_argument("--process", type=Path, required=True) + parser.add_argument("--report", type=Path, required=True) + args = parser.parse_args(argv) + if any( + args.report.resolve().is_relative_to(path.resolve()) + for path in (args.wasm, args.process) + ): + parser.error("report must remain outside canonical input bundles") + try: + result = qualify(args.wasm, args.process) + args.report.parent.mkdir(parents=True, exist_ok=True) + args.report.write_text(json.dumps(result, indent=2) + "\n") + except ( + bundle.BundleError, + OSError, + KeyError, + ValueError, + subprocess.TimeoutExpired, + ) as error: + parser.exit(1, f"runtime qualification refused: {error}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/tools/test_extension_benchmark.py b/tests/tools/test_extension_benchmark.py new file mode 100644 index 00000000..00abb49d --- /dev/null +++ b/tests/tools/test_extension_benchmark.py @@ -0,0 +1,33 @@ +"""Keep timing fixtures within the supported Python/IR roundtrip subset.""" + +from __future__ import annotations + +import pytest + +from morphir_tools import python as api +from scripts.benchmark_extension import sources + + +@pytest.mark.parametrize("version", tuple(api.IrVersion)) +@pytest.mark.parametrize("records,functions", [(1, 1), (200, 100)]) +@pytest.mark.parametrize("with_values", [False, True]) +def test_benchmark_fixtures_roundtrip_in_both_ir_versions( + version: api.IrVersion, records: int, functions: int, with_values: bool +) -> None: + """Numeric suffixes previously caused PY003 in classic v3 word arrays.""" + types, values = sources(records, functions) + compiled = api.compile_sources( + [api.SourceDocument("models.py", values if with_values else types)], + package="acme", + ir_version=version, + ) + assert isinstance(compiled, (api.CompiledV3, api.CompiledV4)), compiled + generated = api.generate_python(compiled.distribution) + assert isinstance(generated, api.Generated), generated + again = api.compile_sources( + [api.SourceDocument(item.path, item.text) for item in generated.artifacts], + package="acme", + ir_version=version, + ) + assert isinstance(again, (api.CompiledV3, api.CompiledV4)), again + assert again.distribution == compiled.distribution diff --git a/tests/tools/test_runtime_choice.py b/tests/tools/test_runtime_choice.py new file mode 100644 index 00000000..526af61e --- /dev/null +++ b/tests/tools/test_runtime_choice.py @@ -0,0 +1,36 @@ +"""Exercise real installed runtime selection and byte-integrity refusals.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + + +@pytest.mark.skipif( + not ( + os.environ.get("MORPHIR_TEST_WASM_BUNDLE") + and os.environ.get("MORPHIR_TEST_PROCESS_BUNDLE") + ), + reason="requires exact built WASM and process bundles plus the pinned host CLI", +) +def test_installed_runtime_choices_are_equivalent_and_tamper_is_refused() -> None: + from scripts.qualify_runtime import qualify + + report = qualify( + Path(os.environ["MORPHIR_TEST_WASM_BUNDLE"]), + Path(os.environ["MORPHIR_TEST_PROCESS_BUNDLE"]), + ) + assert [step["runtime"] for step in report["switches"]] == [ + "wasm", + "process", + "wasm", + ] + assert report["switches"][0]["results"] == report["switches"][1]["results"] + assert ( + report["switches"][2]["results"]["v4"]["ir_sha256"] + == report["switches"][0]["results"]["v4"]["ir_sha256"] + ) + assert report["tampered_install_refused"] is True + assert report["unsupported_platform_refused"] is True diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index d672818f..f48b3a16 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -14,7 +14,7 @@ def test_version_is_defined(self) -> None: def test_version_matches_expected(self) -> None: """The version should match the expected value.""" - assert morphir_tools.__version__ == "0.1.0" + assert morphir_tools.__version__ == "0.1.1" def test_installed_distribution_name(self) -> None: """The tools import belongs to the morphir-python distribution.""" diff --git a/tests/unit/test_extension_bundle.py b/tests/unit/test_extension_bundle.py index c4d81cee..05d4fac1 100644 --- a/tests/unit/test_extension_bundle.py +++ b/tests/unit/test_extension_bundle.py @@ -2,7 +2,9 @@ from __future__ import annotations +import io import json +import zipfile from typing import TYPE_CHECKING import pytest @@ -51,3 +53,80 @@ def test_bundle_requires_multi_document_claim( else: with pytest.raises(bundle.BundleError, match="claims lack"): bundle.receipt(tmp_path) + + +def test_windows_cli_archive_is_verified_and_only_the_binary_is_extracted( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(bundle.platform, "system", lambda: "Windows") + monkeypatch.setattr(bundle.platform, "machine", lambda: "AMD64") + monkeypatch.setattr(bundle, "CACHE", tmp_path) + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("release/morphir.exe", b"verified CLI") + archive.writestr("../../unwanted", b"do not extract") + archive_bytes = stream.getvalue() + digest = bundle.sha256(archive_bytes) + monkeypatch.setattr( + bundle, + "declaration", + lambda: { + "host": { + "version": "test", + "release": "https://example.invalid/releases", + "sha256": {"x86_64-pc-windows-msvc": digest}, + } + }, + ) + urls: list[str] = [] + + def download(url: str, *, timeout: int) -> io.BytesIO: + assert timeout > 0 + urls.append(url) + return io.BytesIO(archive_bytes) + + monkeypatch.setattr(bundle.urllib.request, "urlopen", download) + executable, actual_digest = bundle.morphir_cli() + assert actual_digest == digest + assert executable.name == "morphir.exe" + assert executable.read_bytes() == b"verified CLI" + assert urls == [ + "https://example.invalid/releases/morphir-test-x86_64-pc-windows-msvc.zip" + ] + assert sorted( + p.relative_to(tmp_path).as_posix() for p in tmp_path.rglob("*") if p.is_file() + ) == [ + "morphir-test-x86_64-pc-windows-msvc/morphir-test-x86_64-pc-windows-msvc.zip", + "morphir-test-x86_64-pc-windows-msvc/morphir.exe", + ] + + executable.write_bytes(b"changed cached CLI") + with pytest.raises(bundle.BundleError, match="cached Morphir CLI"): + bundle.morphir_cli() + assert len(urls) == 1 + + executable.write_bytes(b"verified CLI") + (executable.parent / "morphir-test-x86_64-pc-windows-msvc.zip").write_bytes( + b"changed archive" + ) + with pytest.raises(bundle.BundleError, match="digest differs"): + bundle.morphir_cli() + assert len(urls) == 1 + + +def test_integrity_refusal_requires_the_mismatch_and_both_actual_hashes() -> None: + expected, observed = "a" * 64, "b" * 64 + assert bundle.is_digest_mismatch( + f"digest mismatch: expected {expected[:32]}\n│ {expected[32:]}, got {observed}", + expected, + observed, + ) + assert not bundle.is_digest_mismatch( + "cannot read morphir-integrity/sha256/log", expected, observed + ) + assert not bundle.is_digest_mismatch( + f"digest mismatch: expected {expected}", expected, observed + ) + assert not bundle.is_digest_mismatch( + f"invalid module at sha256/{expected}", expected, observed + ) diff --git a/tests/unit/test_mep_evidence.py b/tests/unit/test_mep_evidence.py new file mode 100644 index 00000000..779b5bf0 --- /dev/null +++ b/tests/unit/test_mep_evidence.py @@ -0,0 +1,423 @@ +"""Bind native OS arguments without rewriting canonical MCK report identities.""" + +import copy +import json +from typing import TYPE_CHECKING, Any + +import pytest + +from scripts import extension_bundle as bundle +from scripts import qualify_mep + +if TYPE_CHECKING: + from pathlib import Path + + +def test_native_argument_blobs_preserve_unicode_bytes_and_utf16_units() -> None: + path = "guest-λ.wasm" + assert qualify_mep.argument_blob(path, windows=False) == { + "Unix": list(path.encode()) + } + assert qualify_mep.argument_blob(path, windows=True) == { + "Windows": [ord(character) for character in path] + } + assert qualify_mep.argument_blob("🙂", windows=True) == { + "Windows": [0xD83D, 0xDE42] + } + + +@pytest.fixture +def witness(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + """A small transfer fixture, never a canonical conformance report.""" + declaration = copy.deepcopy(bundle.declaration()) + records = [ + {"caseId": "describe", "observed": {"result": {"fixture": "claims"}}}, + {"caseId": "opaque-request-id", "observed": {"id": "λ"}}, + ] + declaration["mep"]["required_cases"] = len(records) + declaration["mep"]["case_set_sha256"] = qualify_mep.case_set_sha(records) + monkeypatch.setattr(bundle, "declaration", lambda: declaration) + pin = declaration["mep"] + roles = { + "guest": {"path": "/producer/guest", "sha256": "a" * 64}, + "compilerLock": { + "path": "/producer/rust/Cargo.lock", + "sha256": qualify_mep.mep_tool.sha(bundle.ROOT / "rust/Cargo.lock"), + }, + } + identity = { + "runner": { + "name": "morphir", + "version": pin["runner_version"], + "commit": pin["rev"], + "dirty": False, + }, + "runnerBinary": {"path": "/producer/morphir", "sha256": "b" * 64}, + "host": roles["guest"], + "arguments": [], + "files": list(roles.values()), + } + report = { + "contractVersion": "0.1.0-draft.1", + "suite": "mep-guest", + "profile": pin["profile"], + "corpusSha256": pin["corpus_sha256"], + "qualified": True, + "error": None, + "records": records, + "identity": identity, + } + path = tmp_path / "mep-report.json" + path.write_text(json.dumps(report)) + receipt = { + "kind": "canonical-mep-producer-recheck", + "sourceCommit": "c" * 40, + "runtime": "process", + "platform": "aarch64-apple-darwin", + "artifactSha256": "a" * 64, + "definitionInputs": qualify_mep.definition_hashes(), + "reportSha256": qualify_mep.mep_tool.sha(path), + "reportFilename": path.name, + "recheck": { + "exitCode": 0, + "stdout": "MEP guest report: qualified\n", + "stderr": "", + }, + "checker": { + "revision": pin["rev"], + "tree": pin["tree"], + "cargoLockSha256": pin["cargo_lock_sha256"], + "binarySha256": "b" * 64, + "executable": "/producer/morphir", + }, + "checkedIdentity": copy.deepcopy(identity), + "roles": copy.deepcopy(roles), + "invocation": {"host": "/producer/guest", "arguments": [], "windows": False}, + } + return {"receipt": receipt, "report": report, "path": path} + + +def verify(witness: dict[str, Any]) -> dict[str, Any]: + return qualify_mep.verify_transfer( + witness["receipt"], + witness["path"], + commit="c" * 40, + runtime=witness["receipt"]["runtime"], + platform=witness["receipt"]["platform"], + artifact_sha="a" * 64, + claims={"fixture": "claims"}, + ) + + +def reseal(witness: dict[str, Any]) -> None: + witness["path"].write_text(json.dumps(witness["report"])) + witness["receipt"]["reportSha256"] = qualify_mep.mep_tool.sha(witness["path"]) + + +def test_transfer_preserves_original_foreign_paths(witness: dict[str, Any]) -> None: + assert verify(witness) == witness["report"] + + +@pytest.mark.parametrize( + "field", + [ + "sourceCommit", + "artifactSha256", + "definitionInputs", + "reportSha256", + "reportFilename", + "recheck", + "checker", + "checkedIdentity", + "roles", + "invocation", + ], +) +def test_missing_producer_evidence_blocks_transfer( + witness: dict[str, Any], + field: str, +) -> None: + del witness["receipt"][field] + with pytest.raises(bundle.BundleError): + verify(witness) + + +@pytest.mark.parametrize( + "mutation", + [ + "source", + "artifact", + "definitions", + "checker", + "failed-recheck", + "pending-recheck", + "report-bytes", + "corpus", + "profile", + "case-set", + "duplicate-case", + "missing-case", + "claims", + "lock", + "extra-file", + "arguments", + "host", + "dirty-runner", + ], +) +def test_altered_evidence_blocks_transfer_even_when_qualified( + witness: dict[str, Any], + mutation: str, +) -> None: + receipt, report = witness["receipt"], witness["report"] + if mutation == "source": + receipt["sourceCommit"] = "d" * 40 + elif mutation == "artifact": + receipt["artifactSha256"] = "d" * 64 + elif mutation == "definitions": + receipt["definitionInputs"] = {} + elif mutation == "checker": + receipt["checker"]["revision"] = "d" * 40 + elif mutation == "failed-recheck": + receipt["recheck"]["exitCode"] = 1 + elif mutation == "pending-recheck": + receipt["recheck"]["stdout"] = "pending" + elif mutation == "report-bytes": + witness["path"].write_text("{}") + with pytest.raises(bundle.BundleError): + verify(witness) + return + elif mutation == "corpus": + report["corpusSha256"] = "d" * 64 + elif mutation == "profile": + report["profile"] = "moonbit-rich-ion" + elif mutation == "case-set": + report["records"][1]["caseId"] = "unknown" + elif mutation == "duplicate-case": + report["records"][1]["caseId"] = "describe" + elif mutation == "missing-case": + report["records"].pop() + elif mutation == "claims": + report["records"][0]["observed"]["result"] = {} + elif mutation == "lock": + receipt["roles"]["compilerLock"]["sha256"] = "d" * 64 + elif mutation == "extra-file": + report["identity"]["files"].append({"path": "/extra", "sha256": "d" * 64}) + elif mutation == "arguments": + report["identity"]["arguments"] = [ + qualify_mep.argument_blob("x", windows=False) + ] + elif mutation == "host": + report["identity"]["host"] = {"path": "/other", "sha256": "a" * 64} + elif mutation == "dirty-runner": + report["identity"]["runner"]["dirty"] = True + reseal(witness) + with pytest.raises(bundle.BundleError): + verify(witness) + + +def test_live_roles_use_physical_files(tmp_path: Path) -> None: + guest = tmp_path / "guest" + guest.write_bytes(b"actual artifact") + alias = tmp_path / "alias" + alias.symlink_to(guest) + record = {"path": str(alias), "sha256": qualify_mep.mep_tool.sha(guest)} + assert qualify_mep._bind_roles( + {"guest": {"path": str(guest), "sha256": record["sha256"]}}, + {"files": [record]}, + ) == {"guest": record} + + +def test_windows_paths_do_not_use_collector_filesystem() -> None: + assert qualify_mep._same_recorded_path( + "C:/runner/guest.exe", + r"\\?\C:\runner\guest.exe", + windows=True, + ) + assert not qualify_mep._same_recorded_path( + "C:/runner/guest.exe", + "C:/other/guest.exe", + windows=True, + ) + + +def wasm_witness(witness: dict[str, Any]) -> None: + receipt, report = witness["receipt"], witness["report"] + roles = receipt["roles"] + roles["adapter"] = {"path": "/producer/adapter", "sha256": "d" * 64} + for name, relative in ( + ("adapterLock", "tools/extension-qualification/Cargo.lock"), + ("adapterSource", "tools/extension-qualification/src/bin/mep_wasm_host.rs"), + ): + roles[name] = { + "path": f"/producer/{relative}", + "sha256": qualify_mep.mep_tool.sha(bundle.ROOT / relative), + } + receipt["runtime"], receipt["platform"] = "wasm", "portable" + receipt["invocation"] = { + "host": roles["adapter"]["path"], + "arguments": [roles["guest"]["path"]], + "windows": False, + } + report["identity"]["host"] = roles["adapter"] + report["identity"]["arguments"] = [ + qualify_mep.argument_blob(roles["guest"]["path"], windows=False) + ] + report["identity"]["files"] = list(roles.values()) + receipt["checkedIdentity"] = copy.deepcopy(report["identity"]) + reseal(witness) + + +def test_wasm_transfer_binds_adapter_and_guest_argument( + witness: dict[str, Any], +) -> None: + wasm_witness(witness) + assert verify(witness) == witness["report"] + # Even a matching receipt/report cannot substitute another argument for the + # concrete guest whose bytes the producer checked. + witness["receipt"]["invocation"]["arguments"] = ["/producer/other.wasm"] + witness["report"]["identity"]["arguments"] = [ + qualify_mep.argument_blob("/producer/other.wasm", windows=False) + ] + witness["receipt"]["checkedIdentity"] = copy.deepcopy(witness["report"]["identity"]) + reseal(witness) + with pytest.raises(bundle.BundleError): + verify(witness) + + +def test_failed_saved_recheck_never_emits_a_receipt( + witness: dict[str, Any], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + import subprocess + + candidate = tmp_path / "candidate" + candidate.mkdir() + (candidate / "guest").write_bytes(b"guest") + monkeypatch.setattr( + qualify_mep, + "_descriptor", + lambda *_: { + "gitCommit": "c" * 40, + "version": bundle.crate_version(), + "artifacts": [ + { + "platform": bundle._target(), + "filename": "guest", + "sha256": "a" * 64, + "claims": {"fixture": "claims"}, + } + ], + }, + ) + monkeypatch.setattr( + qualify_mep, + "_tool", + lambda _: { + "executable": "checker", + "kit": "kit", + "corpusSha256": "b" * 64, + }, + ) + results = iter( + [ + subprocess.CompletedProcess([], 0, "qualified", ""), + subprocess.CompletedProcess([], 1, "", "identity no longer matches"), + ] + ) + monkeypatch.setattr(qualify_mep.subprocess, "run", lambda *_, **__: next(results)) + output = tmp_path / "recheck.json" + with pytest.raises(bundle.BundleError, match="recheck failed"): + qualify_mep.qualify( + candidate, + "process", + tmp_path / "tool.json", + witness["path"], + output, + "c" * 40, + ) + assert not output.exists() + + +@pytest.mark.parametrize( + "missing", + [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "portable", + ], +) +def test_release_requires_every_platform_and_wasm_proof( + witness: dict[str, Any], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + missing: str, +) -> None: + from scripts.process_extension import PLATFORMS + + inputs = tmp_path / "inputs" + wasm_bundle, wasm_proofs = tmp_path / "wasm", tmp_path / "wasm-proofs" + monkeypatch.setattr( + qualify_mep, + "_descriptor", + lambda directory, _: ( + qualify_mep.mep_tool.object_file(directory / "release.json") + ), + ) + missing_path = None + for target in (*PLATFORMS, "portable"): + item = copy.deepcopy(witness) + is_wasm = target == "portable" + if is_wasm: + wasm_witness(item) + directory, evidence, prefix = wasm_bundle, wasm_proofs, "bundle-mep" + else: + root = inputs / f"morphir-python-process-{target}" + directory = root / ".morphir/build/extensions/python-process" + evidence = root / ".dev/qualification" + prefix = f"mep-{target}" + directory.mkdir(parents=True) + evidence.mkdir(parents=True) + guest = directory / "guest" + guest.write_bytes(target.encode()) + digest = qualify_mep.mep_tool.sha(guest) + receipt, report = item["receipt"], item["report"] + receipt["platform"] = target + receipt["artifactSha256"] = digest + receipt["roles"]["guest"]["sha256"] = digest + receipt["invocation"]["windows"] = target.endswith("windows-msvc") + report["identity"]["files"] = list(receipt["roles"].values()) + report["identity"]["host"] = receipt["roles"]["adapter" if is_wasm else "guest"] + receipt["checkedIdentity"] = copy.deepcopy(report["identity"]) + item["path"] = evidence / f"{prefix}-report.json" + receipt["reportFilename"] = item["path"].name + reseal(item) + path = evidence / f"{prefix}-recheck.json" + path.write_text(json.dumps(receipt)) + (directory / "release.json").write_text( + json.dumps( + { + "gitCommit": "c" * 40, + "artifacts": [ + { + "platform": target, + "filename": "guest", + "sha256": digest, + "claims": {"fixture": "claims"}, + } + ], + } + ) + ) + if target == missing: + missing_path = path + result = qualify_mep.verify_release(inputs, wasm_bundle, wasm_proofs, "c" * 40) + assert len(result["producerRechecks"]) == 6 + assert missing_path is not None + missing_path.unlink() + with pytest.raises((bundle.BundleError, OSError)): + qualify_mep.verify_release(inputs, wasm_bundle, wasm_proofs, "c" * 40) diff --git a/tests/unit/test_mep_tool.py b/tests/unit/test_mep_tool.py new file mode 100644 index 00000000..b68e275a --- /dev/null +++ b/tests/unit/test_mep_tool.py @@ -0,0 +1,99 @@ +"""Source-built checker identities must survive later Cargo rebuilds.""" + +from __future__ import annotations + +import os +import subprocess +import sys +from typing import TYPE_CHECKING, Any + +import pytest + +from scripts import extension_bundle as bundle +from scripts import mep_tool + +if TYPE_CHECKING: + from pathlib import Path + + +def test_frozen_checker_keeps_old_report_identity_when_build_output_changes( + tmp_path: Path, +) -> None: + executable = tmp_path / "morphir" + executable.write_bytes(b"qualified checker") + first = mep_tool.freeze_binary(executable, tmp_path / "cache") + executable.write_bytes(b"a later compatible rebuild") + second = mep_tool.freeze_binary(executable, tmp_path / "cache") + assert first != second + assert first.read_bytes() == b"qualified checker" + assert second.read_bytes() == b"a later compatible rebuild" + assert first.parent.name == mep_tool.sha(first) + assert mep_tool.freeze_binary(executable, tmp_path / "cache") == second + second.write_bytes(b"changed stored checker") + with pytest.raises(bundle.BundleError, match="integrity"): + mep_tool.freeze_binary(executable, tmp_path / "cache") + + +def test_unicode_report_does_not_depend_on_producer_locale(tmp_path: Path) -> None: + report = tmp_path / "report.json" + report.write_bytes('{"opaqueId":"λ雪🙂"}'.encode()) + result = subprocess.run( + [ + sys.executable, + "-c", + "from pathlib import Path; import sys; " + "from scripts.mep_tool import object_file; " + "assert object_file(Path(sys.argv[1]))['opaqueId'] == " + "'\\u03bb\\u96ea\\U0001f642'", + str(report), + ], + env=os.environ + | { + "LC_ALL": "C", + "PYTHONUTF8": "0", + "PYTHONCOERCECLOCALE": "0", + }, + capture_output=True, + timeout=10, + ) + assert result.returncode == 0, result.stderr + + +def test_failed_build_exposes_diagnostics_and_retains_full_log( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + diagnostic = ( + "earlier build output\n" * 100 + "error: missing Windows prerequisite λ\n" + ).encode() + + def fail(arguments: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]: + kwargs["stdout"].write(diagnostic) + return subprocess.CompletedProcess(arguments, 1) + + monkeypatch.setattr(mep_tool.subprocess, "run", fail) + log = tmp_path / "build.log" + with pytest.raises(bundle.BundleError, match="missing Windows prerequisite λ"): + mep_tool.build_checker(tmp_path, tmp_path / "target", log) + assert log.read_bytes() == diagnostic + + +def test_short_cargo_cache_is_scoped_to_the_checker_build( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + original, short = tmp_path / "ordinary-cache", tmp_path / "short" + monkeypatch.setenv("CARGO_HOME", str(original)) + monkeypatch.setenv("MORPHIR_MCK_CARGO_HOME", str(short)) + environments = [] + + def compile( + arguments: list[str], **kwargs: Any + ) -> subprocess.CompletedProcess[str]: + environments.append(kwargs["env"]) + return subprocess.CompletedProcess(arguments, 0) + + monkeypatch.setattr(mep_tool.subprocess, "run", compile) + mep_tool.build_checker(tmp_path, tmp_path / "target", tmp_path / "build.log") + assert environments[0]["CARGO_HOME"] == str(short.resolve()) + assert os.environ["CARGO_HOME"] == str(original) diff --git a/tests/unit/test_native_qualification.py b/tests/unit/test_native_qualification.py index 3e3623ad..8dade6d9 100644 --- a/tests/unit/test_native_qualification.py +++ b/tests/unit/test_native_qualification.py @@ -111,3 +111,33 @@ def test_rust_free_path_drops_directories_holding_only_rustup( rustup.chmod(0o755) monkeypatch.setenv("PATH", f"{tools}{os.pathsep}/usr/bin") assert str(tools) not in q.rust_free_path(tmp_path / "venv").split(os.pathsep) + + +def test_mismatched_install_does_not_modify_hardlinked_wheel_cache( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + import subprocess + + cached = tmp_path / "cached-metadata" + original = "Name: morphir-python-native\nVersion: 0.1.1\n" + cached.write_text(original) + installed = tmp_path / "installed.dist-info" + installed.mkdir() + metadata = installed / "METADATA" + metadata.hardlink_to(cached) + + def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + if "from importlib.metadata" in command[-1]: + return subprocess.CompletedProcess( + command, 0, stdout=str(installed), stderr="" + ) + assert "Version: 0.0.0" in metadata.read_text() + return subprocess.CompletedProcess( + command, 1, stdout="", stderr="morphir-python-native 0.0.0" + ) + + monkeypatch.setattr(q.subprocess, "run", run) + monkeypatch.setattr(q, "_script", lambda python, name: "not-executed") + q._mismatch(tmp_path / "python", "empty-path") + assert cached.read_text() == original + assert "Version: 0.0.0" in metadata.read_text() diff --git a/tests/unit/test_process_extension.py b/tests/unit/test_process_extension.py new file mode 100644 index 00000000..19b2eb97 --- /dev/null +++ b/tests/unit/test_process_extension.py @@ -0,0 +1,116 @@ +"""Process release receipts bind canonical descriptors and exact executable bytes.""" + +from __future__ import annotations + +import json +import os +from typing import TYPE_CHECKING + +import pytest + +from scripts import extension_bundle as wasm +from scripts import process_extension as process + +if TYPE_CHECKING: + from pathlib import Path + + +def candidate(directory: Path) -> str: + version = wasm.crate_version() + name = process.artifact_name(version, "aarch64-apple-darwin") + binary = directory / name + binary.write_bytes(b"executable fixture") + binary.chmod(0o700) + digest = wasm.sha256(binary.read_bytes()) + (directory / f"{name}.sha256").write_text(f"{digest} {name}\n") + (directory / "release.json").write_text( + json.dumps( + { + "schemaVersion": "2.0.0-draft.2", + "extensionId": "morphir-python", + "shortId": "python", + "version": version, + "gitCommit": "a" * 40, + "platformDifferences": "none", + "artifacts": [ + { + "runtime": "process", + "platform": "aarch64-apple-darwin", + "filename": name, + "sha256": digest, + "claimCheck": "unchecked", + "claims": { + "protocolVersions": wasm.declaration()["extension"][ + "mep_versions" + ], + "extension": {"id": "morphir-python", "version": version}, + "capabilities": {}, + }, + } + ], + } + ) + ) + return name + + +def test_process_receipt_binds_source_bytes_launch_and_mode(tmp_path: Path) -> None: + name = candidate(tmp_path) + receipt = process.receipt(tmp_path, "a" * 40, "aarch64-apple-darwin") + assert receipt["sourceCommit"] == "a" * 40 + assert receipt["arguments"] == [] + assert receipt["explicitArguments"] == ["--stdio"] + assert receipt["runtime"] == "process" + assert receipt["files"][name] == wasm.sha256((tmp_path / name).read_bytes()) + assert receipt["executable"] is True + + +@pytest.mark.parametrize( + "change", + [ + "bytes", + "digest", + "source", + "platform", + "runtime", + "claims", + "missing", + "extra", + "mode", + ], +) +def test_process_receipt_refuses_mismatched_candidates( + tmp_path: Path, change: str +) -> None: + name = candidate(tmp_path) + descriptor = json.loads((tmp_path / "release.json").read_text()) + if change == "bytes": + (tmp_path / name).write_bytes(b"changed") + elif change == "digest": + (tmp_path / f"{name}.sha256").write_text("wrong") + elif change == "source": + descriptor["gitCommit"] = "b" * 40 + elif change == "platform": + descriptor["artifacts"][0]["platform"] = "x86_64-pc-windows-msvc" + elif change == "runtime": + descriptor["artifacts"][0]["runtime"] = "wasm" + elif change == "claims": + descriptor["artifacts"][0]["claims"]["extension"]["version"] = "0.0.0" + elif change == "missing": + (tmp_path / name).unlink() + elif change == "extra": + (tmp_path / "unqualified").touch() + elif change == "mode": + if os.name == "nt": + pytest.skip("POSIX executable mode") + (tmp_path / name).chmod(0o600) + (tmp_path / "release.json").write_text(json.dumps(descriptor)) + with pytest.raises(wasm.BundleError): + process.receipt(tmp_path, "a" * 40, "aarch64-apple-darwin") + + +def test_process_platforms_are_explicit_and_windows_uses_exe() -> None: + assert len(process.PLATFORMS) == 5 + assert process.artifact_name("0.4.2", "x86_64-pc-windows-msvc").endswith(".exe") + with pytest.raises(wasm.BundleError, match="unsupported process platform"): + process.artifact_name("0.4.2", "riscv64-unknown-linux-gnu") diff --git a/tests/unit/test_process_release.py b/tests/unit/test_process_release.py new file mode 100644 index 00000000..8b7d3ba2 --- /dev/null +++ b/tests/unit/test_process_release.py @@ -0,0 +1,415 @@ +"""A release must contain the same qualified candidate on every platform.""" + +from __future__ import annotations + +import copy +import json +import tarfile +from typing import TYPE_CHECKING, Any + +import pytest + +from scripts import extension_bundle as bundle +from scripts import process_diagnostics +from scripts import process_extension as process +from scripts import process_release as release +from scripts.qualify_native import tools_pin + +if TYPE_CHECKING: + from pathlib import Path + +COMMIT = "a" * 40 + + +def candidates(root: Path) -> tuple[dict[str, Any], dict[str, Any]]: + version = bundle.crate_version() + claims = { + "protocolVersions": bundle.declaration()["extension"]["mep_versions"], + "extension": {"id": "morphir-python", "version": version}, + "capabilities": {}, + } + results = { + v: { + "modules": ["acme/models.py", "acme/rules.py"], + "agrees_with_python_api": True, + } + for v in ["v3", "v4"] + } + for target in process.PLATFORMS: + platform_root = root / f"morphir-python-process-{target}" + directory = platform_root / ".morphir/build/extensions/python-process" + directory.mkdir(parents=True) + name = process.artifact_name(version, target) + executable = directory / name + executable.write_bytes(target.encode()) + executable.chmod(0o700) + digest = bundle.sha256(executable.read_bytes()) + (directory / f"{name}.sha256").write_text(f"{digest} {name}\n") + descriptor = { + "schemaVersion": "2.0.0-draft.2", + "shortId": "python", + "extensionId": "morphir-python", + "version": version, + "gitCommit": COMMIT, + "platformDifferences": "none", + "artifacts": [ + { + "runtime": "process", + "platform": target, + "filename": name, + "sha256": digest, + "claims": claims, + "claimCheck": "unchecked", + } + ], + } + (directory / "release.json").write_text(json.dumps(descriptor)) + receipt = process.receipt(directory, COMMIT, target) + archive_digest = bundle.declaration()["host"]["sha256"][target] + receipt["bundler"] = { + "version": bundle.declaration()["host"]["version"], + "archiveSha256": archive_digest, + "binarySha256": "b" * 64, + } + receipt["cargoLockSha256"] = bundle.sha256( + (bundle.ROOT / "rust/Cargo.lock").read_bytes() + ) + receipt["processDiagnostics"] = { + "scope": "legacy-process-diagnostic-parity", + "artifactSha256": digest, + "checkerSha256": bundle.sha256( + (bundle.ROOT / "scripts/process_diagnostics.py").read_bytes() + ), + "inputSha256": bundle.sha256(process_diagnostics.SOURCE.encode()), + "compilerVersion": version, + "nativeVersion": tools_pin(), + "protocolVersion": "0.1", + "cleanEnvironment": True, + "exitCode": 0, + "stderr": "", + "cases": { + version: { + "diagnostics": [{"code": "PY004", "severity": "error"}], + "pythonApi": [{"code": "PY004", "severity": "error"}], + } + for version in ("3", "4") + }, + } + receipt["installedHost"] = { + **copy.deepcopy(receipt), + "host": { + "morphir": bundle.declaration()["host"]["version"], + "sha256": archive_digest, + "binary_sha256": "b" * 64, + }, + "python_api": {"native_version": tools_pin(), "compiler_version": version}, + "offline_round_trips": results, + "installed_integrity": { + "expected_sha256": digest, + "tampered_sha256": "e" * 64, + "exit_code": 1, + "stdout": "", + "stderr": f"digest mismatch: expected {digest}, got {'e' * 64}", + "fresh_project": True, + }, + "qualification_script_sha256": bundle.sha256( + (bundle.ROOT / "scripts/extension_bundle.py").read_bytes() + ), + } + path = platform_root / f".dev/qualification/process-{target}.json" + path.parent.mkdir(parents=True) + path.write_text(json.dumps(receipt)) + if target.endswith("windows-msvc"): + (path.parent / "windows-process-imports.json").write_text( + json.dumps( + { + "sourceCommit": COMMIT, + "crt": "static", + "artifact": {"sha256": digest}, + "checkerSha256": bundle.sha256( + ( + bundle.ROOT + / ".github/scripts/check_process_windows.ps1" + ).read_bytes() + ), + "tool": { + "name": "dumpbin.exe", + "sha256": "d" * 64, + "version": "test", + }, + "dependencies": ["kernel32.dll", "ntdll.dll"], + } + ) + ) + # Artifact transfer preserves bytes, but not POSIX execute bits. + executable.chmod(0o600) + return claims, results + + +def test_complete_process_set_keeps_canonical_per_platform_artifacts( + tmp_path: Path, +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + report = release.assemble(inputs, output, COMMIT, claims, results) + assert report["sourceCommit"] == COMMIT + assert report["platforms"] == list(process.PLATFORMS) + assert len(report["files"]) == 11 + descriptor = json.loads((output / "release.json").read_text()) + assert len(descriptor["artifacts"]) == 5 + assert {artifact["runtime"] for artifact in descriptor["artifacts"]} == {"process"} + assert all(artifact["claims"] == claims for artifact in descriptor["artifacts"]) + + +def archive_candidate(tmp_path: Path) -> tuple[Path, Path, Path]: + inputs, directory = tmp_path / "inputs", tmp_path / "canonical" + claims, results = candidates(inputs) + report = release.assemble(inputs, directory, COMMIT, claims, results) + wasm = tmp_path / "wasm" + wasm.mkdir() + version = bundle.crate_version() + name = f"morphir-python-binding-{version}.wasm" + digest = bundle.sha256(b"WASM fixture") + (wasm / name).write_bytes(b"WASM fixture") + (wasm / f"{name}.sha256").write_text(f"{digest} {name}\n") + (wasm / "release.json").write_text( + json.dumps( + { + "extensionId": "morphir-python", + "version": version, + "gitCommit": COMMIT, + "artifacts": [ + { + "runtime": "wasm", + "filename": name, + "sha256": digest, + "claims": claims, + } + ], + } + ) + ) + assembled = tmp_path / "assembly.json" + assembled.write_text(json.dumps(report)) + return directory, assembled, wasm + + +def test_download_archive_keeps_exact_bytes_modes_and_canonical_descriptor( + tmp_path: Path, +) -> None: + directory, assembled, wasm = archive_candidate(tmp_path) + version = bundle.crate_version() + report = json.loads(assembled.read_text()) + first = release.archive_bundle( + directory, assembled, wasm, tmp_path / "assets", COMMIT + ) + second = release.archive_bundle( + directory, assembled, wasm, tmp_path / "again", COMMIT + ) + assert first == second + assert first["sourceCommit"] == COMMIT + assert len(list((tmp_path / "assets").iterdir())) == 14 + with tarfile.open( + tmp_path / "assets" / f"morphir-python-extension-{version}.tar.gz" + ) as archive: + assert set(archive.getnames()) == set(report["files"]) + for member in archive.getmembers(): + stream = archive.extractfile(member) + assert stream is not None + assert bundle.sha256(stream.read()) == report["files"][member.name] + assert member.isfile() + assert member.uid == member.gid == member.mtime == 0 + assert member.mode == ( + 0o644 if member.name.endswith((".json", ".sha256")) else 0o700 + ) + lines = (tmp_path / "assets/SHA256SUMS").read_text().splitlines() + assert len(lines) == 13 + for line in lines: + checksum, filename = line.split(" ") + assert checksum == bundle.sha256((tmp_path / "assets" / filename).read_bytes()) + + +@pytest.mark.parametrize( + "change", + ["bytes", "receipt", "source", "claims", "platform", "extra", "symlink", "copy"], +) +def test_archive_refuses_changed_inputs_or_transfers_without_partial_assets( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, change: str +) -> None: + directory, assembled, wasm = archive_candidate(tmp_path) + executable = directory / process.artifact_name( + bundle.crate_version(), process.PLATFORMS[0] + ) + report = json.loads(assembled.read_text()) + descriptor = json.loads((directory / "release.json").read_text()) + if change == "bytes": + executable.write_bytes(b"changed bytes") + elif change == "receipt": + report["files"][executable.name] = "c" * 64 + elif change == "source": + report["sourceCommit"] = "c" * 40 + elif change in ("claims", "platform"): + if change == "claims": + descriptor["artifacts"][0]["claims"]["capabilities"]["streaming"] = True + else: + descriptor["artifacts"][0]["platform"] = descriptor["artifacts"][1][ + "platform" + ] + (directory / "release.json").write_text(json.dumps(descriptor)) + report["files"]["release.json"] = bundle.sha256( + (directory / "release.json").read_bytes() + ) + elif change == "extra": + (directory / "unqualified").write_bytes(b"extra") + elif change == "symlink": + target = tmp_path / "outside" + executable.rename(target) + executable.symlink_to(target) + elif change == "copy": + original = release.shutil.copyfile + + def changed_copy(source: Path, destination: Path) -> None: + original(source, destination) + destination.write_bytes(b"changed transfer") + + monkeypatch.setattr(release.shutil, "copyfile", changed_copy) + assembled.write_text(json.dumps(report)) + with pytest.raises((bundle.BundleError, OSError)): + release.archive_bundle(directory, assembled, wasm, tmp_path / "assets", COMMIT) + assert not (tmp_path / "assets").exists() + + +@pytest.mark.parametrize( + "change", + [ + "missing", + "extra", + "source", + "lock", + "files", + "claims", + "host", + "api", + "v4", + "results", + "bytes", + "checker", + "binary", + "windows-runtime", + "windows-bytes", + "windows-checker", + "windows-missing", + "integrity-digest", + "integrity-not-run", + "integrity-cache", + "integrity-reason", + "diagnostics-missing", + "diagnostics-code", + "diagnostics-api", + "diagnostics-checker", + "diagnostics-artifact", + "diagnostics-version", + "diagnostics-environment", + "diagnostics-v4", + ], +) +def test_mixed_missing_or_unqualified_process_sets_are_refused( + tmp_path: Path, change: str +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + target = process.PLATFORMS[0] + platform_root = inputs / f"morphir-python-process-{target}" + receipt_path = platform_root / f".dev/qualification/process-{target}.json" + receipt = json.loads(receipt_path.read_text()) + if change == "diagnostics-missing": + receipt.pop("processDiagnostics", None) + elif change.startswith("diagnostics-"): + proof = receipt["processDiagnostics"] + if change == "diagnostics-code": + proof["cases"]["3"]["diagnostics"][0]["code"] = "PY001" + proof["cases"]["3"]["pythonApi"][0]["code"] = "PY001" + elif change == "diagnostics-api": + proof["cases"]["3"]["pythonApi"][0]["code"] = "PY001" + elif change == "diagnostics-checker": + proof["checkerSha256"] = "c" * 64 + elif change == "diagnostics-artifact": + proof["artifactSha256"] = "c" * 64 + elif change == "diagnostics-version": + proof["compilerVersion"] = "0.0.0" + elif change == "diagnostics-environment": + proof["cleanEnvironment"] = False + elif change == "diagnostics-v4": + del proof["cases"]["4"] + elif change.startswith("integrity-"): + proof = receipt["installedHost"]["installed_integrity"] + if change == "integrity-digest": + proof["expected_sha256"] = "c" * 64 + elif change == "integrity-not-run": + proof["exit_code"] = 0 + elif change == "integrity-cache": + proof["fresh_project"] = False + elif change == "integrity-reason": + proof["stderr"] = "unrelated permission error" + elif change.startswith("windows-"): + windows = ( + inputs + / "morphir-python-process-x86_64-pc-windows-msvc/.dev/qualification/windows-process-imports.json" + ) + proof = json.loads(windows.read_text()) + if change == "windows-missing": + windows.unlink() + else: + if change == "windows-runtime": + proof["dependencies"].append("VCRUNTIME140.dll") + elif change == "windows-bytes": + proof["artifact"]["sha256"] = "c" * 64 + elif change == "windows-checker": + proof["checkerSha256"] = "c" * 64 + windows.write_text(json.dumps(proof)) + elif change == "missing": + receipt_path.unlink() + elif change == "extra": + (inputs / "morphir-python-process-unknown").mkdir() + elif change == "source": + receipt["sourceCommit"] = "c" * 40 + elif change == "lock": + receipt["cargoLockSha256"] = "c" * 64 + elif change == "files": + receipt["files"]["release.json"] = "c" * 64 + elif change == "claims": + receipt["claims"]["capabilities"]["progress"] = True + elif change == "host": + receipt["bundler"]["archiveSha256"] = "c" * 64 + elif change == "checker": + receipt["installedHost"]["qualification_script_sha256"] = "c" * 64 + elif change == "binary": + receipt["installedHost"]["host"]["binary_sha256"] = "c" * 64 + elif change == "api": + receipt["installedHost"]["python_api"]["native_version"] = "0.0.0" + elif change == "v4": + del receipt["installedHost"]["offline_round_trips"]["v4"] + elif change == "results": + receipt["installedHost"]["offline_round_trips"]["v4"]["modules"] = [] + elif change == "bytes": + name = process.artifact_name(bundle.crate_version(), target) + (platform_root / ".morphir/build/extensions/python-process" / name).write_bytes( + b"changed executable" + ) + if change != "missing": + receipt_path.write_text(json.dumps(receipt)) + with pytest.raises((bundle.BundleError, OSError)): + release.assemble(inputs, output, COMMIT, claims, results) + assert not output.exists() + + +def test_process_assembly_does_not_overwrite_an_existing_directory( + tmp_path: Path, +) -> None: + inputs, output = tmp_path / "inputs", tmp_path / "release" + claims, results = candidates(inputs) + output.mkdir() + (output / "keep").write_text("user data") + with pytest.raises(FileExistsError): + release.assemble(inputs, output, COMMIT, claims, results) + assert (output / "keep").read_text() == "user data" diff --git a/tools/extension-qualification/Cargo.lock b/tools/extension-qualification/Cargo.lock new file mode 100644 index 00000000..09159e55 --- /dev/null +++ b/tools/extension-qualification/Cargo.lock @@ -0,0 +1,5310 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.1", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "cap-fs-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476f0d0003a760918ed4b1e039a59e11769030416f79c8222551d22785f7f70d" +dependencies = [ + "cap-primitives 3.4.6", + "cap-std 3.4.6", + "io-lifetimes 2.0.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-fs-ext" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ff379b70af8e08307a8f65e7040c7301cb4a572538ade16b4984f0da77847f" +dependencies = [ + "cap-primitives 4.0.3", + "cap-std 4.0.3", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras 0.19.0", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec6a5b75f54547c579a6b117c6fdd5f04f4ab7598de747b9f440a53592b3a4a" +dependencies = [ + "ambient-authority", + "rand 0.8.8", +] + +[[package]] +name = "cap-std" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" +dependencies = [ + "cap-primitives 3.4.6", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "rustix 1.1.4", +] + +[[package]] +name = "cap-std" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" +dependencies = [ + "cap-primitives 4.0.3", + "io-extras 0.19.0", + "io-lifetimes 3.0.1", + "rustix 1.1.4", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54c289326c70f1c697ebf0a31842a480932e5942b5fac92fcc46e87286b48e2" +dependencies = [ + "ambient-authority", + "cap-primitives 3.4.6", + "iana-time-zone", + "once_cell", + "rustix 1.1.4", + "winx", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cbindgen" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20" +dependencies = [ + "heck", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" + +[[package]] +name = "cranelift-control" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" + +[[package]] +name = "cranelift-native" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "delegate" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e018fccbeeb50ff26562ece792ed06659b9c2dae79ece77c4456bb10d9bf79b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", + "zeroize", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "extism" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b66cd9ac5c64b49c9bac69db3d1b10d8f9386e7caab73489c2f197ba43d5e05" +dependencies = [ + "anyhow", + "async-trait", + "cbindgen", + "extism-convert", + "extism-manifest", + "glob", + "libc", + "serde", + "serde_json", + "sha2 0.10.9", + "toml 0.9.12+spec-1.1.0", + "tracing", + "tracing-subscriber", + "ureq", + "url", + "uuid", + "wasi-common", + "wasmtime", + "wiggle", +] + +[[package]] +name = "extism-convert" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad19858c4c462309a8f3a20abec53e8603bda1eefda26c8bfab51d5516b40cbb" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytemuck", + "extism-convert-macros", + "prost", + "rmp-serde", + "serde", + "serde_json", +] + +[[package]] +name = "extism-convert-macros" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2932799f6d9f9646f97b65287f6bb2addc75a0ee61e40fb24559a7540dd928" +dependencies = [ + "manyhow", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "extism-manifest" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f59c8dadb5e0bde9a48c6ed45312e6ef625cbcd5f67c28459dbc8fe8bc0383" +dependencies = [ + "base64 0.22.1", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352fcb5a66eb74145a1c4a01f2bd15d59c62c85be73aac8471880c65b26b798f" +dependencies = [ + "anyhow", + "base64 0.22.1", + "extism-convert", + "extism-manifest", + "extism-pdk-derive", + "serde", + "serde_json", +] + +[[package]] +name = "extism-pdk-derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d086daea5fd844e3c5ac69ddfe36df4a9a43e7218cf7d1f888182b089b09806c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.13.1", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ice_code" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6524844f553e8da5999f3000cf11d3f1ff926bb03fc087441c7b86dee4a7d48" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes 2.0.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ion-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285b4429808446f30da668d210beaeb7ab82defb6045e261c6cdfaeafe565552" +dependencies = [ + "arrayvec", + "base64 0.12.3", + "bumpalo", + "chrono", + "compact_str", + "delegate", + "generic-array", + "ice_code", + "memchr", + "num-bigint 0.4.8", + "num-integer", + "num-traits", + "phf", + "rustc-hash", + "smallvec", + "thiserror 1.0.69", + "visibility", + "winnow 0.6.24", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jupyter-protocol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670197bfdc3b171334f450d9a3ae47999e8aec048579cc3c52f752cc8ff59843" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "serde", + "serde_json", + "thiserror 2.0.21", + "uuid", +] + +[[package]] +name = "lasso" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" +dependencies = [ + "dashmap", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b" +dependencies = [ + "sha2 0.11.0", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.1.4", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "morphir-common" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "anyhow", + "cap-fs-ext 4.0.3", + "cap-std 4.0.3", + "dirs", + "flate2", + "fs2", + "glob", + "indexmap", + "ion-rs", + "libc", + "morphir-config", + "morphir-core", + "morphir-workspace", + "nbformat", + "reqwest", + "rustix 1.1.4", + "same-file", + "secrecy", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "stacker", + "tar", + "tempfile", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", + "tracing", + "unicode-casefold", + "unicode-normalization", + "uuid", + "vfs", + "windows-sys 0.61.2", + "zip", +] + +[[package]] +name = "morphir-config" +version = "0.0.1" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "secrecy", + "serde", + "serde-saphyr", + "serde_json", + "thiserror 2.0.21", + "toml 1.1.6+spec-1.1.0", +] + +[[package]] +name = "morphir-core" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "anyhow", + "bigdecimal", + "granit-parser", + "indexmap", + "lasso", + "morphir-json", + "num-bigint 0.5.1", + "schemars", + "serde", + "serde_json", + "sha2 0.11.0", + "stacker", + "thiserror 2.0.21", +] + +[[package]] +name = "morphir-distribution" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "flate2", + "fs2", + "morphir-common", + "morphir-extension-sdk", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "tar", + "tempfile", + "thiserror 2.0.21", + "tough", + "tracing", + "unicode-casefold", + "unicode-normalization", + "url", + "zip", +] + +[[package]] +name = "morphir-extension-sdk" +version = "0.4.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "base64 0.22.1", + "extism-pdk", + "ion-rs", + "morphir-json", + "morphir-workspace", + "percent-encoding", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "morphir-host" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "async-lock", + "async-trait", + "morphir-core", + "morphir-extension-sdk", + "morphir-workspace", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "morphir-host-native" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "async-trait", + "base64 0.23.1", + "extism", + "morphir-common", + "morphir-core", + "morphir-distribution", + "morphir-extension-sdk", + "morphir-host", + "morphir-workspace", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.21", + "tokio", + "tracing", + "unicode-casefold", + "unicode-normalization", +] + +[[package]] +name = "morphir-json" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "morphir-python-host-qualification" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "hex", + "morphir-extension-sdk", + "morphir-host", + "morphir-host-native", + "serde", + "serde_json", + "sha2 0.11.0", + "tempfile", + "tokio", +] + +[[package]] +name = "morphir-workspace" +version = "0.2.0" +source = "git+https://github.com/finos/morphir-rust.git?rev=d51daa8aea7ae09ae295ff254d72b27770c64104#d51daa8aea7ae09ae295ff254d72b27770c64104" +dependencies = [ + "globset", + "morphir-config", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "nbformat" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c06543f305caee515923b125a737c4d58079c063e25701f93f45077bb8d622" +dependencies = [ + "anyhow", + "chrono", + "jupyter-protocol", + "serde", + "serde_json", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.8", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "futures-core", + "pin-project", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags 2.13.1", + "cap-fs-ext 3.4.6", + "cap-std 3.4.6", + "fd-lock", + "io-lifetimes 2.0.4", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tough" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b378d98765c2ae9cdc3e9963ea7e670da8cdd9ee39611b8d722083c7f1ac11" +dependencies = [ + "async-recursion", + "async-trait", + "aws-lc-rs", + "bytes", + "dyn-clone", + "futures", + "futures-core", + "globset", + "hex", + "jiff", + "log", + "olpc-cjson", + "pem", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "serde_plain", + "snafu", + "tempfile", + "tokio", + "tokio-util", + "typed-path 0.9.3", + "untrusted 0.7.1", + "url", + "walkdir", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82205ffd44a9697e34fc145491aa47310f9871540bb7909eaa9365e0a9a46607" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-casefold" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f66b1c8f8caa2ab31dc6d3f35386f16efdab89668f93411e565ac368908e8f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64 0.23.1", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vfs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d33cbe2ac48f2a446f04c33aef4906078ffd224888df5ae60b7ed401ded771" +dependencies = [ + "filetime", +] + +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi-common" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46137f5bcc41a0f002ed14688e463665388a6f3a6662a12a8c315d4b8849791c" +dependencies = [ + "async-trait", + "bitflags 2.13.1", + "cap-fs-ext 3.4.6", + "cap-rand", + "cap-std 3.4.6", + "cap-time-ext", + "fs-set-times", + "io-extras 0.18.4", + "io-lifetimes 2.0.4", + "log", + "rustix 1.1.4", + "system-interface", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.16.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags 2.13.1", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmtime" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" +dependencies = [ + "addr2line", + "async-trait", + "bitflags 2.13.1", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.38.1", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.1.4", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap", + "log", + "object 0.38.1", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2 0.10.9", + "smallvec", + "target-lexicon", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" +dependencies = [ + "base64 0.22.1", + "directories-next", + "log", + "postcard", + "rustix 1.1.4", + "serde", + "serde_derive", + "sha2 0.10.9", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object 0.38.1", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix 1.1.4", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" +dependencies = [ + "cc", + "object 0.38.1", + "rustix 1.1.4", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object 0.38.1", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object 0.38.1", + "target-lexicon", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "heck", + "indexmap", + "wit-parser", +] + +[[package]] +name = "wast" +version = "35.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" +dependencies = [ + "leb128", +] + +[[package]] +name = "wast" +version = "259.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69beba8d9da07af9a0971b559149a2ac4729895b55144de4fddbf5a02660648" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.259.0", +] + +[[package]] +name = "wat" +version = "1.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eec44b0c80391b20fb7ad9ea440c72c5d382385bdf74444bb18321c425017e" +dependencies = [ + "wast 259.0.0", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wiggle" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cfd3db2f05619c6f36f257d84327c11546e28d61e3a1c1220aaad553bc4b0" +dependencies = [ + "bitflags 2.13.1", + "thiserror 2.0.21", + "tracing", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", + "witx", +] + +[[package]] +name = "wiggle-generate" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd7a197903e5b4ff5e13aef9c891960d71e92073600ecf4c86c7e795ac1c803" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-environ", + "witx", +] + +[[package]] +name = "wiggle-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6410b86fcec207070d9372b215d3470bad67215e6bbac46981a16999c4abbc28" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "wiggle-generate", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.3", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path 0.12.3", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/tools/extension-qualification/Cargo.toml b/tools/extension-qualification/Cargo.toml new file mode 100644 index 00000000..3bf0b3bb --- /dev/null +++ b/tools/extension-qualification/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "morphir-python-host-qualification" +version = "0.1.0" +edition = "2024" +rust-version = "1.98" +license = "Apache-2.0" +publish = false + +# Independent developer tooling, not part of the compiler/Python dependency tree. +[workspace] + +[dependencies] +anyhow = "1" +async-trait = "0.1" +hex = "0.4" +morphir-extension-sdk = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +morphir-host = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +morphir-host-native = { git = "https://github.com/finos/morphir-rust.git", rev = "d51daa8aea7ae09ae295ff254d72b27770c64104" } +serde_json = { version = "1", features = ["arbitrary_precision"] } +serde = { version = "1", features = ["derive"] } +sha2 = "0.11" +tempfile = "3" +tokio = { version = "1", features = ["rt", "macros", "sync", "time"] } diff --git a/tools/extension-qualification/README.md b/tools/extension-qualification/README.md new file mode 100644 index 00000000..14eacd00 --- /dev/null +++ b/tools/extension-qualification/README.md @@ -0,0 +1,105 @@ +# Source-pinned Rust host qualification + +This developer-only executable exercises the actual WASM and native process +guest bytes through `morphir-host` and `morphir-host-native` at +`d51daa8aea7ae09ae295ff254d72b27770c64104`. It uses the upstream Extism and +process channels directly. Process launches have an empty environment and do +not use Python or a Rust launcher. + +The Git dependencies are isolated in this tool's Cargo workspace. The compiler +and Python packages continue to depend on published crates. A source-built host +does not remove the compiler's published SDK prerequisite. + +Build once, then supply the exact candidate artifacts: + +```bash +cargo build --locked --release --manifest-path tools/extension-qualification/Cargo.toml \ + --target-dir .dev/host-qualification-target + +.dev/host-qualification-target/release/morphir-python-host-qualification \ + path/to/guest.wasm path/to/morphir-python-extension \ + .dev/qualification/rust-host.json +``` + +The checker owns temporary copies, verifies their hashes, and restores only +the process copy's executable permission if artifact transfer removed it. It +initializes each runtime using legacy MEP `0.1`, then checks IR v3/v4 compilation, +generation, recompilation, unsupported-source diagnostics, matching results, +and orderly shutdown. The process route must leave no running child, unread +stdout, or stderr. An actual guest request with a 1 ms host deadline must fail +with a timeout and leave the child reaped. The timeout can occur during +initialization or during the intentionally large compile request. + +The JSON result binds the checker binary, artifacts, inputs, Rust toolchain and +host revision. This legacy interoperability check is separate from the pinned +executable MCK guest suite. It cannot establish the newer MEP draft, negotiated +model transport, canonical conformance, installed-byte integrity or platform +coverage. Those have their own required checks. + +## Repeat timing observations + +```bash +python3 -m scripts.benchmark_extension \ + --checker .dev/host-qualification-target/release/morphir-python-host-qualification \ + --wasm path/to/guest.wasm --process path/to/morphir-python-extension \ + --output .dev/qualification/runtime-timings --samples 5 +``` + +The destination must be new. The runner retains both Python inputs, every raw +checker result and log, hashes, the machine description, and nanosecond samples +with min/median/max in `timings.json`. Workloads contain one record/one function +and 200 records/100 typed conditional functions. Alphabetic identifiers fit +both supported IR naming formats. Every sample still verifies semantic and +diagnostic parity and process cleanup. + +Each sample starts a fresh checker process and fresh guests. Guest startup +includes initialization and, for WASM, module compilation. OS caches are not +cleared. Operations run in a fixed session order: v3 types, v3 values, v4 types, +v4 values. Later operations reuse the guest session. WASM runs before process, +so the report records that order and makes no general speed claim. Whole-checker +wall time includes both runtimes, validation, timeout cleanup and report I/O; +it is not a per-runtime startup measurement. + +The checker also accepts two optional source paths after `REPORT`, for the +type-only and type/value cases respectively. The benchmark uses these to keep +the measured inputs reproducible. Inputs must compile and roundtrip in both +advertised IR versions. + +## Bridge the real WASM guest to the shared MCK + +The same build produces `mep_wasm_host`, a transport adapter for the upstream +`morphir mck mep guest-run` executable. It forwards request bodies unchanged to +the actual Extism guest's `handle` export and returns its exact response bytes. +The adapter contains no conformance expectations or adjudication logic. + +```bash +"$MCK" --no-banner mck mep guest-run --kit "$PYTHON_JSON_KIT" \ + --host "$PWD/.dev/host-qualification-target/release/mep_wasm_host" \ + --host-arg "$WASM_GUEST" --identity-file "$WASM_GUEST" \ + --identity-file "$PWD/rust/Cargo.lock" \ + --identity-file "$PWD/tools/extension-qualification/Cargo.lock" \ + --identity-file "$PWD/tools/extension-qualification/src/bin/mep_wasm_host.rs" \ + --report "$PWD/.dev/qualification/mck-wasm.json" +"$MCK" --no-banner mck mep report-check --kit "$PYTHON_JSON_KIT" \ + "$PWD/.dev/qualification/mck-wasm.json" +``` + +Use an explicitly pinned shared MCK source/binary and its matching +`python-json-library-v1` kit. Supply absolute guest paths. Process qualification +can pass the raw executable directly as `--host`, without this adapter. +The saved report records the actual host executable, arguments, guest bytes, +checker, corpus, identities and observations. Its canonical recheck requires +the same checker and all recorded identity paths while they are available. + +The release producers automate this through `scripts.mep_tool` and +`scripts.qualify_mep`. Each producer rechecks its saved report locally before +uploading it. The Linux collector verifies the source/artifact/report identities +and successful producer receipt from the same workflow run; it does not rewrite +foreign filesystem paths or claim to re-run their filesystem recheck. See the +[release contract](../../docs/releasing.md#qualify-both-extension-runtimes). + +Extism exposes a requests-only `handle` ABI. As in the pinned upstream +`ExtismChannel`, notifications are not forwarded; an exit notification closes +the bridge and unloads the guest. Those lifecycle observations qualify this +adapter/guest pair, rather than claiming guest notification delivery. Process +session tests exercise actual native guest notification handling separately. diff --git a/tools/extension-qualification/build.rs b/tools/extension-qualification/build.rs new file mode 100644 index 00000000..abd49eff --- /dev/null +++ b/tools/extension-qualification/build.rs @@ -0,0 +1,17 @@ +fn main() { + let rustc = std::env::var("RUSTC").expect("Cargo identifies its compiler"); + let output = std::process::Command::new(rustc) + .arg("-vV") + .output() + .expect("read compiler identity"); + assert!(output.status.success()); + let version = String::from_utf8(output.stdout) + .expect("compiler identity is UTF-8") + .replace('\n', " | "); + println!("cargo:rustc-env=QUALIFICATION_RUSTC={version}"); + for variable in ["PROFILE", "OPT_LEVEL", "DEBUG"] { + let value = std::env::var(variable).expect("Cargo identifies its build settings"); + println!("cargo:rustc-env=QUALIFICATION_{variable}={value}"); + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/tools/extension-qualification/src/bin/mep_wasm_host.rs b/tools/extension-qualification/src/bin/mep_wasm_host.rs new file mode 100644 index 00000000..e9b852ea --- /dev/null +++ b/tools/extension-qualification/src/bin/mep_wasm_host.rs @@ -0,0 +1,126 @@ +//! Thin Extism/stdio adapter for the shared executable MCK. +//! No expectations or conformance decisions live here; raw request/response +//! bodies pass through the actual guest's exported `handle` function. +use anyhow::{Context, Result, ensure}; +use morphir_host_native::extism::{ExtensionContainer, MorphirHostFunctions}; +use serde::{Deserialize, Deserializer}; +use serde_json::value::RawValue; +use std::{ + fs, + io::{BufRead, Write}, + path::PathBuf, +}; + +const MAX_HEADER: usize = 8192; +const MAX_BODY: usize = 16 * 1024 * 1024; + +#[derive(Deserialize)] +struct Head { + method: String, + #[serde(default, deserialize_with = "present_id")] + id: Option>, +} + +fn present_id<'de, D: Deserializer<'de>>(input: D) -> Result>, D::Error> { + Box::::deserialize(input).map(Some) +} + +fn read_frame(input: &mut impl BufRead) -> Result>> { + let mut header = Vec::new(); + loop { + let available = input.fill_buf()?; + if available.is_empty() { + ensure!(header.is_empty(), "truncated MCK adapter header"); + return Ok(None); + } + let byte = available[0]; + input.consume(1); + header.push(byte); + ensure!(header.len() <= MAX_HEADER, "MCK adapter header exceeds8KiB"); + if header.ends_with(b"\r\n\r\n") { + break; + } + } + let mut length = None; + for line in std::str::from_utf8(&header)?.trim_end().split("\r\n") { + let (name, value) = line.split_once(':').context("invalid MCK adapter header")?; + if name.eq_ignore_ascii_case("content-length") { + ensure!(length.is_none(), "duplicate Content-Length"); + let value = value.trim(); + ensure!(!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())); + let size: usize = value.parse()?; + ensure!(size <= MAX_BODY, "MCK adapter body exceeds16MiB"); + length = Some(size); + } + } + let mut body = vec![0; length.context("missing Content-Length")?]; + input.read_exact(&mut body)?; + Ok(Some(body)) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let mut args = std::env::args_os().skip(1); + let wasm = args + .next() + .map(PathBuf::from) + .context("usage: mep_wasm_host GUEST.wasm")?; + ensure!(args.next().is_none(), "unexpected adapter argument"); + // Use the same pinned concrete host container as interoperability checks. + // Notifications have no Extism request ABI, matching ExtismChannel policy. + let container = ExtensionContainer::from_bytes( + "morphir-python", + &fs::read(wasm)?, + MorphirHostFunctions::default(), + )?; + let mut input = std::io::stdin().lock(); + let mut output = std::io::stdout().lock(); + while let Some(body) = read_frame(&mut input)? { + let head: Head = serde_json::from_slice(&body)?; + if head.id.is_none() { + if head.method == "morphir.exit" { + break; + } + continue; + } + let response = container.call_raw("handle", &body).await?; + ensure!(response.len() <= MAX_BODY, "guest response exceeds16MiB"); + write!(output, "Content-Length: {}\r\n\r\n", response.len())?; + output.write_all(&response)?; + output.flush()?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn unicode_body_uses_byte_lengths_and_concatenated_frames_keep_boundaries() { + let body = "λ".as_bytes(); + let mut bytes = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes(); + bytes.extend(body); + bytes.extend(b"Content-Length: 2\r\n\r\n{}"); + let mut input = Cursor::new(bytes); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), body); + assert_eq!(read_frame(&mut input).unwrap().unwrap(), b"{}"); + assert!(read_frame(&mut input).unwrap().is_none()); + } + + #[test] + fn invalid_truncated_duplicate_and_unbounded_frames_are_refused() { + for bytes in [ + b"Content-Length: 2\r\n\r\n{".as_slice(), + b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}", + b"Content-Length: -1\r\n\r\n", + b"Content-Length: 999999999999999999999999\r\n\r\n", + b"Content-Length: 16777217\r\n\r\n", + b"Content-Length: 2\r\n", + ] { + assert!(read_frame(&mut Cursor::new(bytes)).is_err()); + } + assert!(read_frame(&mut Cursor::new(vec![b'x'; MAX_HEADER + 1])).is_err()); + } +} diff --git a/tools/extension-qualification/src/main.rs b/tools/extension-qualification/src/main.rs new file mode 100644 index 00000000..535097d9 --- /dev/null +++ b/tools/extension-qualification/src/main.rs @@ -0,0 +1,383 @@ +//! Independent source-pinned Rust host qualification of actual guest bytes. +//! This verifies legacy interoperability; it is not the canonical MCK guest kit. +use anyhow::{Context, Result, bail, ensure}; +use async_trait::async_trait; +use morphir_extension_sdk::{ + CompileOptions, CompilePackage, CompileRequest, GenerateRequest, SourceDocument, SourceSet, + protocol::{ExtensionResponse, PeerInfo}, +}; +use morphir_host::{ + Channel, ChannelError, ChannelState, ExpectedChecks, ExpectedExtension, HostConfig, + JsonRpcConnection, Outgoing, Session, +}; +use morphir_host_native::{ + CheckedConnection, + extism::{ExtensionContainer, ExtismChannel, MorphirHostFunctions}, + process::{ProcessChannel, ProcessLaunch}, +}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::{ + fs, + io::Read, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::Mutex; + +const HOST_SOURCE: &str = "d51daa8aea7ae09ae295ff254d72b27770c64104"; +const ID: &str = "morphir-python"; +const TYPES_SOURCE: &str = + "from dataclasses import dataclass\n@dataclass(frozen=True)\nclass Box:\n value: int\n"; +const FUNCTION_SOURCE: &str = "\ndef choose(flag: bool, first: int, second: int) -> int:\n return first if flag else second\n"; + +#[derive(Clone)] +struct SharedProcess(Arc>); + +#[async_trait] +impl Channel for SharedProcess { + async fn send(&mut self, message: Outgoing) -> Result<(), ChannelError> { + self.0.lock().await.send(message).await + } + async fn receive(&mut self) -> Result { + self.0.lock().await.receive().await + } + async fn close(&mut self) -> Result { + self.0.lock().await.close().await + } + async fn abort(&mut self) -> Result { + self.0.lock().await.abort().await + } +} + +fn host_config() -> HostConfig { + HostConfig::new(PeerInfo { + name: "morphir-python-qualification".into(), + version: env!("CARGO_PKG_VERSION").into(), + kind: Default::default(), + }) +} + +fn request(version: &str, types_only: bool) -> CompileRequest { + CompileRequest { + language_id: "python".into(), + sources: SourceSet { + root: None, + documents: vec![SourceDocument { + uri: "models.py".into(), + language_id: "python".into(), + version: 1, + text: if types_only { + TYPES_SOURCE.into() + } else { + format!("{TYPES_SOURCE}{FUNCTION_SOURCE}") + }, + }], + }, + package: CompilePackage { + name: "acme".into(), + exposed_modules: None, + }, + dependencies: vec![], + options: CompileOptions { + ir_version: version.into(), + types_only, + ..Default::default() + }, + baseline: None, + } +} + +fn hash_file(path: &Path) -> Result { + let mut file = fs::File::open(path)?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 65536]; + loop { + let size = file.read(&mut buffer)?; + if size == 0 { + break; + } + digest.update(&buffer[..size]); + } + Ok(hex::encode(digest.finalize())) +} + +fn canonical_digest(value: &Value) -> Result { + let mut canonical = value.clone(); + canonical.sort_all_objects(); + Ok(hex::encode(Sha256::digest(serde_json::to_vec(&canonical)?))) +} + +async fn process_session( + executable: &Path, + cwd: &Path, + timeout: Duration, +) -> Result<(Session, SharedProcess)> { + // ProcessLaunch inherits no user environment. No Python or Rust launcher. + let launch = ProcessLaunch::new(ID, executable, cwd).request_timeout(timeout); + let channel = ProcessChannel::spawn(launch).await?; + let expected = channel.expectation(); + let shared = SharedProcess(Arc::new(Mutex::new(channel))); + let connection = CheckedConnection::new(JsonRpcConnection::new( + shared.clone(), + ExpectedChecks::new(expected), + )); + let session = Session::open(connection, &host_config()).await?; + Ok((session, shared)) +} + +async fn qualify_runtime( + wasm: &Path, + process: &Path, + runtime: &str, + types_source: &str, + values_source: &str, +) -> Result { + let work = tempfile::tempdir()?; + let started = Instant::now(); + let (mut session, child) = if runtime == "process" { + let (session, child) = + process_session(process, work.path(), Duration::from_secs(30)).await?; + (session, Some(child)) + } else { + let bytes = fs::read(wasm)?; + let container = + ExtensionContainer::from_bytes(ID, &bytes, MorphirHostFunctions::default())?; + let channel = ExtismChannel::new(container, ExpectedExtension::identified(ID)); + let expected = channel.expectation(); + let connection = CheckedConnection::new(JsonRpcConnection::new( + channel, + ExpectedChecks::new(expected), + )); + (Session::open(connection, &host_config()).await?, None) + }; + let startup_ns = started.elapsed().as_nanos(); + ensure!(session.negotiated().protocol_version() == "0.1"); + let mut operations = Vec::new(); + for version in ["3", "4"] { + for types_only in [true, false] { + let compile_start = Instant::now(); + let mut input = request(version, types_only); + input.sources.documents[0].text = if types_only { + types_source.into() + } else { + values_source.into() + }; + let compiled = session.compile(input).await?; + let compile_ns = compile_start.elapsed().as_nanos(); + ensure!(compiled.success, "compile: {:?}", compiled.diagnostics); + let ir = compiled.ir.context("successful compile has IR")?; + let generate_start = Instant::now(); + let generated = session + .generate(GenerateRequest { + ir: ir.clone(), + target: "python".into(), + options: Default::default(), + }) + .await?; + let generate_ns = generate_start.elapsed().as_nanos(); + ensure!(generated.success, "generate: {:?}", generated.diagnostics); + let mut recompile = request(version, types_only); + recompile.sources.documents = generated + .artifacts + .iter() + .map(|artifact| SourceDocument { + uri: artifact.path.clone(), + language_id: "python".into(), + version: 1, + text: artifact.content.clone(), + }) + .collect(); + let again = session.compile(recompile).await?; + ensure!( + again.success && again.ir.as_ref() == Some(&ir), + "generated source changed IR" + ); + let mut invalid = request(version, types_only); + invalid.sources.documents[0].text = "import os\n".into(); + let refused = session.compile(invalid).await?; + ensure!(!refused.success && refused.ir.is_none() && !refused.diagnostics.is_empty()); + operations.push(json!({ + "irVersion":version, "typesOnly":types_only, + "irSha256":canonical_digest(&ir)?, + "artifacts":generated.artifacts, "diagnostics":refused.diagnostics, + "compileNs":compile_ns, "generateNs":generate_ns, + })); + } + } + session.close().await?; + if let Some(child) = child { + let mut channel = child.0.lock().await; + ensure!(!channel.is_running()?, "closed guest is still running"); + ensure!( + channel.stdout_is_exhausted().await?, + "stdout contains unread bytes" + ); + ensure!( + channel.stderr_output().is_empty(), + "unexpected guest diagnostics" + ); + } + Ok( + json!({"runtime":runtime,"protocolVersion":"0.1","startupNs":startup_ns,"operations":operations,"orderlyClose":true}), + ) +} + +async fn timeout_cleanup(process: &Path) -> Result { + let work = tempfile::tempdir()?; + let channel = ProcessChannel::spawn( + ProcessLaunch::new(ID, process, work.path()).request_timeout(Duration::from_millis(1)), + ) + .await?; + let expected = channel.expectation(); + let shared = SharedProcess(Arc::new(Mutex::new(channel))); + let connection = CheckedConnection::new(JsonRpcConnection::new( + shared.clone(), + ExpectedChecks::new(expected), + )); + let (phase, reason) = match Session::open(connection, &host_config()).await { + Err(error) => ("initialize", error.to_string()), + Ok(mut session) => { + let mut large = request("4", true); + let mut source = "from dataclasses import dataclass\n".to_owned(); + for index in 0..20000 { + source.push_str(&format!( + "@dataclass(frozen=True)\nclass Item{index}:\n value: int\n" + )); + } + large.sources.documents[0].text = source; + let error = session + .compile(large) + .await + .expect_err("large request should exceed 1ms deadline"); + ("compile", error.to_string()) + } + }; + ensure!( + reason.contains("timed out") || reason.contains("timeout"), + "{reason}" + ); + ensure!( + !shared.0.lock().await.is_running()?, + "timed-out child was not reaped" + ); + Ok(json!({"deadlineMs":1,"phase":phase,"error":reason,"childRunning":false})) +} + +fn equivalent(first: &Value, second: &Value) -> bool { + let mut first = first.clone(); + let mut second = second.clone(); + for value in [&mut first, &mut second] { + for entry in value.as_array_mut().unwrap() { + entry.as_object_mut().unwrap().remove("compileNs"); + entry.as_object_mut().unwrap().remove("generateNs"); + } + } + first == second +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let mut arguments = std::env::args_os().skip(1); + let wasm = arguments + .next() + .map(PathBuf::from) + .context("usage: qualifier WASM PROCESS REPORT [TYPES_SOURCE VALUES_SOURCE]")?; + let process = arguments + .next() + .map(PathBuf::from) + .context("missing process executable")? + .canonicalize()?; + let report = arguments + .next() + .map(PathBuf::from) + .context("missing report path")?; + let sources = if let Some(types_path) = arguments.next() { + let values_path = arguments.next().context("missing values source path")?; + ( + fs::read_to_string(types_path)?, + fs::read_to_string(values_path)?, + ) + } else { + ( + TYPES_SOURCE.into(), + format!("{TYPES_SOURCE}{FUNCTION_SOURCE}"), + ) + }; + if arguments.next().is_some() { + bail!("unexpected arguments"); + } + let wasm = wasm.canonicalize()?; + let owned = tempfile::tempdir()?; + let wasm_copy = owned.path().join("guest.wasm"); + let process_copy = owned + .path() + .join(process.file_name().context("missing process filename")?); + fs::copy(&wasm, &wasm_copy)?; + fs::copy(&process, &process_copy)?; + ensure!(hash_file(&wasm)? == hash_file(&wasm_copy)?); + ensure!(hash_file(&process)? == hash_file(&process_copy)?); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&process_copy, fs::Permissions::from_mode(0o700))?; + } + let wasm = wasm_copy; + let process = process_copy; + let wasm_result = qualify_runtime(&wasm, &process, "wasm", &sources.0, &sources.1).await?; + let process_result = + qualify_runtime(&wasm, &process, "process", &sources.0, &sources.1).await?; + ensure!( + equivalent(&wasm_result["operations"], &process_result["operations"]), + "runtime results or diagnostics differ" + ); + let timeout = timeout_cleanup(&process).await?; + let result = json!({ + "scope":"source-pinned-legacy-host-interoperability", + "hostSource":HOST_SOURCE, "rustc":env!("QUALIFICATION_RUSTC"), + "build":{ + "profile":env!("QUALIFICATION_PROFILE"), + "optLevel":env!("QUALIFICATION_OPT_LEVEL"), + "debug":env!("QUALIFICATION_DEBUG"), + }, + "definitionInputs":{ + "Cargo.toml":hex::encode(Sha256::digest(include_bytes!("../Cargo.toml"))), + "Cargo.lock":hex::encode(Sha256::digest(include_bytes!("../Cargo.lock"))), + "build.rs":hex::encode(Sha256::digest(include_bytes!("../build.rs"))), + "src/main.rs":hex::encode(Sha256::digest(include_bytes!("main.rs"))), + }, + "checkerBinarySha256":hash_file(&std::env::current_exe()?)?, + "wasmSha256":hash_file(&wasm)?, "processSha256":hash_file(&process)?, + "inputs":{ + "typesOnlySha256":hex::encode(Sha256::digest(sources.0.as_bytes())), + "typesAndValuesSha256":hex::encode(Sha256::digest(sources.1.as_bytes())), + }, + "results":[wasm_result,process_result],"timeout":timeout, + }); + if let Some(parent) = report.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&report, serde_json::to_vec_pretty(&result)?)?; + println!( + "host interoperability qualified; report {}", + report.display() + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parity_ignores_observation_times_but_preserves_diagnostics_and_ir() { + let first = json!([{"compileNs":1,"generateNs":2,"irSha256":"abc","diagnostics":[{"code":"PY004"}]}]); + let second = json!([{"compileNs":3,"generateNs":4,"irSha256":"abc","diagnostics":[{"code":"PY004"}]}]); + assert!(equivalent(&first, &second)); + let different_diagnostic = json!([{"compileNs":3,"generateNs":4,"irSha256":"abc","diagnostics":[{"code":"PY003"}]}]); + assert!(!equivalent(&first, &different_diagnostic)); + let different_ir = json!([{"compileNs":3,"generateNs":4,"irSha256":"def","diagnostics":[{"code":"PY004"}]}]); + assert!(!equivalent(&first, &different_ir)); + } +} diff --git a/uv.lock b/uv.lock index 3d9732bb..3881afac 100644 --- a/uv.lock +++ b/uv.lock @@ -201,7 +201,7 @@ source = { editable = "packages/morphir" } [[package]] name = "morphir-python" -version = "0.1.0" +version = "0.1.1" source = { editable = "packages/morphir-tools" } dependencies = [ { name = "morphir" },