diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6684c38..45c785a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,19 @@ jobs: env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_INSTALLER_SIGNING_IDENTITY: ${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }} run: | + if [ -z "$APPLE_INSTALLER_SIGNING_IDENTITY" ]; then + echo "::error::APPLE_INSTALLER_SIGNING_IDENTITY secret is not set" + exit 1 + fi + case "$APPLE_INSTALLER_SIGNING_IDENTITY" in + "Developer ID Installer:"*) ;; + *) + echo "::error::APPLE_INSTALLER_SIGNING_IDENTITY must name a Developer ID Installer identity" + exit 1 + ;; + esac echo "$APPLE_CERTIFICATE" | base64 --decode > cert.p12 KEYCHAIN="preflight-$$.keychain" security create-keychain -p "" "$KEYCHAIN" @@ -375,6 +387,8 @@ jobs: done - name: Build .pkg installer + env: + APPLE_INSTALLER_SIGNING_IDENTITY: ${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }} run: | VERSION="${GITHUB_REF_NAME#v}" bash scripts/build-pkg.sh \ @@ -382,7 +396,7 @@ jobs: "target/release" \ "assets" \ "$VERSION" \ - "${{ secrets.APPLE_INSTALLER_SIGNING_IDENTITY }}" + "$APPLE_INSTALLER_SIGNING_IDENTITY" - name: Verify .pkg payload manifest run: | @@ -420,6 +434,12 @@ jobs: xcrun stapler staple "packages/Capsem-$VERSION.pkg" xcrun stapler validate "packages/Capsem-$VERSION.pkg" + - name: Verify .pkg signature and Gatekeeper acceptance + run: | + VERSION="${GITHUB_REF_NAME#v}" + pkgutil --check-signature "packages/Capsem-$VERSION.pkg" + spctl -a -vv -t install "packages/Capsem-$VERSION.pkg" + - name: Generate SBOM run: cargo sbom --output-format spdx_json_2_3 > capsem-sbom.spdx.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f6e6e..e23e8e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 as the first FAQ. - Added a reusable `.deb` payload verifier and wired release CI to validate Linux package helper binaries, signed manifests, and manifest signatures. +- Added a macOS release CI gate that requires a Developer ID Installer identity + and runs `pkgutil --check-signature` plus Gatekeeper assessment after + notarization and stapling. ### Fixed - Fixed the marketing-site installer for the stamped v1.1 package assets: macOS now installs the downloaded `.pkg` with the native installer, and package downloads are checked against the release manifest when local tools are available. +- Fixed `.deb` payload verification for zstd-compressed packages without an + embedded content-size header, matching the published Debian package format. - Fixed Linux KVM unit-test compilation issues surfaced by PR CI before the site/download installer hardening can merge. - Fixed macOS PR CI's clean-checkout Rust unit gate by creating a minimal diff --git a/pyproject.toml b/pyproject.toml index eff6050..e5801c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "click>=8.0", "jinja2>=3.0", "blake3>=1.0.8", + "zstandard>=0.25.0", ] [project.scripts] diff --git a/scripts/verify_deb_payload.py b/scripts/verify_deb_payload.py index e57c7b9..34bc3c9 100644 --- a/scripts/verify_deb_payload.py +++ b/scripts/verify_deb_payload.py @@ -99,7 +99,8 @@ def _decompress(name: str, payload: bytes) -> bytes: stderr = result.stderr.decode("utf-8", errors="replace") raise VerificationError(f"zstd failed to decompress {name}: {stderr}") return result.stdout - return zstandard.ZstdDecompressor().decompress(payload) + with zstandard.ZstdDecompressor().stream_reader(BytesIO(payload)) as reader: + return reader.read() return payload diff --git a/skills/release-process/SKILL.md b/skills/release-process/SKILL.md index 7d3c29f..91ed56c 100644 --- a/skills/release-process/SKILL.md +++ b/skills/release-process/SKILL.md @@ -167,6 +167,14 @@ Test runs in parallel with builds. A test failure blocks `create-release` but do - **`just cross-compile` is not a perfect CI replica.** It runs in a docker container on macOS, which has FUSE (via Colima's Linux VM). CI runners may not have FUSE, so AppImage bundling that works locally can fail in CI. The recipe catches compile errors and most packaging issues, but environment differences (FUSE, linuxdeploy availability) can still slip through. Always verify the first CI run of a new Linux packaging change. - **Platform-gate all macOS-only APIs.** Every use of `libc::clonefile`, `AppleVzHypervisor`, `core_foundation_sys`, etc. must be wrapped in `#[cfg(target_os = "macos")]` -- struct, impl, AND tests. The Linux app build compiles the full workspace. `cargo test --test platform_gating` catches ungated symbols at unit test time. This burned v0.14.7 through v0.14.9. - **Pin Xcode version on macOS runners.** Always `sudo xcode-select -s /Applications/Xcode_16.2.app` (or latest) before any Apple toolchain use. GitHub periodically updates runner images and the default Xcode can break (Abort trap in xcodebuild). The preflight may pass on one runner instance while build-app-macos gets a different one. v0.14.12 failed because Xcode 15.4's xcodebuild crashed with `Abort trap: 6` when Tauri tried to locate notarytool -- despite zero workflow changes from v0.14.11 which passed 9 hours earlier. +- **Installer identity and Gatekeeper checks are release gates.** Release + preflight must require `APPLE_INSTALLER_SIGNING_IDENTITY`, and it must start + with `Developer ID Installer:`. Pass it into `scripts/build-pkg.sh` through + the job environment, not inline expressions. After `xcrun stapler validate`, + `build-app-macos` must run `pkgutil --check-signature` and + `spctl -a -vv -t install` against the built `.pkg`. If a local macOS host + reports Code Signing subsystem errors for multiple known-good releases, treat + the host as suspect, but keep the CI macOS gate release-blocking. - **`latest.json` is optional in `gh release create`.** Tauri only generates updater `latest.json` for bundle types that produce `.tar.gz` + `.sig` artifacts (AppImage, not deb). With deb-only builds, no `latest.json` exists. The create-release step must handle this gracefully. - **AppImage was dropped after 14 failed releases.** linuxdeploy (a FUSE2 AppImage) cannot run on Ubuntu 24.04 CI runners (FUSE3 only). Tested: `libfuse2` install, `APPIMAGE_EXTRACT_AND_RUN=1` env var, both together -- none worked reliably. If AppImage support is needed in the future, the approach would be to pre-extract linuxdeploy (`--appimage-extract`) and run the extracted binary directly, bypassing FUSE entirely. @@ -220,6 +228,7 @@ Propagation can lag 1-5 min after accepting. `notarytool history` must return a | `APPLE_CERTIFICATE` | Base64 `.p12` (legacy 3DES) | | `APPLE_CERTIFICATE_PASSWORD` | Password for p12 | | `APPLE_SIGNING_IDENTITY` | `Developer ID Application: Elie Bursztein (L8EGK4X86T)` | +| `APPLE_INSTALLER_SIGNING_IDENTITY` | `Developer ID Installer: Elie Bursztein (L8EGK4X86T)` | | `APPLE_API_ISSUER` | App Store Connect issuer UUID | | `APPLE_API_KEY` | App Store Connect key ID | | `APPLE_API_KEY_PATH` | Contents of `.p8` private key | @@ -250,7 +259,9 @@ Use `scripts/verify_deb_payload.py` for `.deb` inspection instead of ad hoc `tar`/`strings` checks. It validates control metadata, companion binaries, the signed manifest files, and optional minisign verification. The manifest signature check is mandatory for local-signature releases; a release is not -verified until `minisign -Vm` passes against `config/manifest-sign.pub`. +verified until `minisign -Vm` passes against `config/manifest-sign.pub`. The +script handles `.tar.zst` Debian payloads with a streaming zstandard reader +because published `.deb` members may omit an embedded content-size header. For a demo-facing macOS release, also prove the installer path users see: diff --git a/sprints/release-policy-hardening/MASTER.md b/sprints/release-policy-hardening/MASTER.md index 5b317b3..3bfbe4e 100644 --- a/sprints/release-policy-hardening/MASTER.md +++ b/sprints/release-policy-hardening/MASTER.md @@ -2,12 +2,12 @@ ## Goal -Prepare the next `1.1.1778456247` release candidate by fixing the blocker-class issues +Prepare the next `1.1.1778542197` release candidate by fixing the blocker-class issues found in the post-sprint swarm review: release artifacts must install and boot on fresh machines, Policy V2 settings must be usable from the UI, hook runtime must fail closed safely, and docs must not overclaim shipped behavior. -T9 selected exact version `1.1.1778456247` and owns keeping the stamp recipe, +T9 selected exact version `1.1.1778542197` and owns keeping the stamp recipe, docs, binaries, and tag plan on that line. ## Status @@ -22,11 +22,11 @@ docs, binaries, and tag plan on that line. | T5 service/process/package helpers | Implementation complete; focused package/VM proof passed in T10 | P0 | T0 package layout, T1 rootfs gate | Focused Rust/Python checks + compile gate + T10 Gate B/E2E/package proof | Helper packaging, spec route/auth proof, rootfs validation, env isolation, async cleanup, and builtin-aware reload/refresh are implemented; clean installed-package launch remains T11. | | T6 telemetry/session tooling | Implementation complete; focused real-session trace proof passed in T10 | P2 | T3/T8 telemetry semantics | Logger/core/service/MCP/session/frontend gates passed + focused T8 timeline assertion | Old/core DB compatibility, Policy V2 schema checks, MCP/tool correlation, dns/hook/audit/snapshot timeline layers, triage, frontend policy fields, lifecycle tests, and legacy migration coverage are implemented. | | T7 swarm intake and review control | Owner mapping complete; downstream closeout open | P0 | T8-T12 downstream resolution | FD01-FD14 transfer trackers + Galileo audit | Final investigation wave and mapping audit are captured; every finding doc is linked to owner T-track rows, while downstream blocker checkboxes stay open until resolved or deferred. | -| T8 policy integration E2E | Implementation complete; focused VM proof passed in T10 | P1 | T2/T3/T5/T6 | Hook defer decision + 388 frontend tests + focused Rust checks + focused VM E2E | Configured external hook dispatch is deferred for `1.1.1778456247`; non-hook Policy V2 settings/reload/timeline E2E path, reload banner dismissal, backend hook rejection, runtime support matrix, and live `/settings` + `/reload-config` MCP E2E proof are implemented. | -| T9 release metadata and changelog | Implementation complete; commit discipline pending | P1 | T0-T8 final decisions | version sync + latest-release extraction + release page + docs build + partial workflow check | Exact `1.1.1778456247` stamp, changelog, latest release, 1.1 release page, lockfile, stamp recipe, and internal dependency metadata are synchronized; workflow preflight still needs local signing prerequisites. | +| T8 policy integration E2E | Implementation complete; focused VM proof passed in T10 | P1 | T2/T3/T5/T6 | Hook defer decision + 388 frontend tests + focused Rust checks + focused VM E2E | Configured external hook dispatch is deferred for `1.1.1778542197`; non-hook Policy V2 settings/reload/timeline E2E path, reload banner dismissal, backend hook rejection, runtime support matrix, and live `/settings` + `/reload-config` MCP E2E proof are implemented. | +| T9 release metadata and changelog | Implementation complete; commit discipline pending | P1 | T0-T8 final decisions | version sync + latest-release extraction + release page + docs build + partial workflow check | Exact `1.1.1778542197` stamp, changelog, latest release, 1.1 release page, lockfile, stamp recipe, and internal dependency metadata are synchronized; workflow preflight still needs local signing prerequisites. | | T10 focused verification | Complete; T11 blockers explicit | P0 | T0-T9 fixes | focused Rust/Python/frontend/docs + `.deb` install + `.pkg` expansion + Gate A/B + T8 E2E proof | Targeted checks, host doctor, strict `just test-install`, `just exec "echo cli-ok"`, `just exec "capsem-doctor"`, focused T8 policy E2E, rootfs validation, `.pkg` expansion/signature proof, frontend coverage, and `just ui` visual evidence are passing/captured; clean installed-package proof remains a T11/manual-host gate. | | T11 local release candidate gate | Full suite/private preflight/install smoke/installed doctor/demo UI green; manual sign-off open | P0 | T10 green | Full `just test`, final doctor, restored-private preflight, VM doctor, Docker install e2e, host install smoke | Final `just test`, final host `just doctor`, direct B3SUMS/signature checks, `just exec "capsem-doctor"`, restored Apple/notary/manifest preflight, `just install`, installed CLI run, installed doctor, rebuilt `.pkg` app-materialization fix, `/Applications` demo UI launch, `just run-ui --` process proof, and installed-app tray relaunch proof passed by 2026-05-11. Remaining blockers are Elie Gate C/Gate D visual sign-off and the no-tag/no-push hold before T12. | -| T12 CI green release landing | Not started | P0 | T11 signed off | 10 CI/live-release gates | Tag `v1.1.1778456247`, CI green, release assets verified, release landed. | +| T12 CI green release landing | Release landed; CI hardening follow-up in progress | P0 | T11 signed off | CI green + live asset verification + follow-up package gates | `v1.1.1778542197` is published/latest, CI and site publish are green, live manifest/packages verify, and follow-up CI now blocks future releases on macOS pkg signature/Gatekeeper checks. | ## Phases @@ -64,7 +64,7 @@ docs, binaries, and tag plan on that line. `tracker.md`. 7. Run T11 locally: full suite, package generation, local install, and Elie + Codex CLI/UI/full-launch verification. -8. Only after T11 is signed off, run T12: tag `v1.1.1778456247`, wait for CI, verify +8. Only after T11 is signed off, run T12: tag `v1.1.1778542197`, wait for CI, verify published assets, and mark the release landed. ## Elie + Codex Manual Gates @@ -150,7 +150,7 @@ The release workflow must fail before publish if any expected item is missing. - `scripts/preflight.sh` and `scripts/check-release-workflow.sh`: verify Apple signing/notarization readiness, Tauri signing readiness if updater remains enabled, and manifest-signing readiness. -- Release job: after tag `v1.1.1778456247`, wait for CI green and require live +- Release job: after tag `v1.1.1778542197`, wait for CI green and require live GitHub release asset verification before declaring the release landed. ## Immediate Release Blockers @@ -299,7 +299,7 @@ findings, and had its output copied into a durable finding doc under - Docs/release notes distinguish shipped hook Spec0/runtime infrastructure from not-yet-wired user/corp hook dispatch. - T8 records that configured external hook dispatch does not ship in - `1.1.1778456247`; UI/docs/tests reject or describe hook dispatch as + `1.1.1778542197`; UI/docs/tests reject or describe hook dispatch as infrastructure-only. - Frontend runtime/image truth has an owner: asset readiness, image/fork UI contract, create defaults, and service/gateway status truth cannot remain @@ -311,5 +311,5 @@ findings, and had its output copied into a durable finding doc under release tagging. - `just install` generates and installs the package locally before CI/tagging, and Elie signs off CLI, JS UI, desktop launch, and installed app behavior. -- T12 waits for CI green and verifies live `v1.1.1778456247` release assets before the +- T12 waits for CI green and verifies live `v1.1.1778542197` release assets before the release is marked landed. diff --git a/sprints/release-policy-hardening/T12-ci-release-landing.md b/sprints/release-policy-hardening/T12-ci-release-landing.md index 8f07ac7..3d15bce 100644 --- a/sprints/release-policy-hardening/T12-ci-release-landing.md +++ b/sprints/release-policy-hardening/T12-ci-release-landing.md @@ -2,7 +2,7 @@ ## Objective -Land the `v1.1.1778456247` release only after T11 has produced a locally installed, +Land the `v1.1.1778542197` release only after T11 has produced a locally installed, Elie-verified release candidate. T12 owns the tag, CI run, live GitHub release assets, post-publish verification, and final "release landed" evidence. @@ -25,7 +25,7 @@ after the fix. - [P0] The sprint previously stopped at a local release hold; it did not have a final sprint that waits for CI green and verifies live release assets. -- [P0] The target release line is `1.1.1778456247`; T12 must fail if tag, changelog, +- [P0] The target release line is `1.1.1778542197`; T12 must fail if tag, changelog, binary metadata, release page, or published assets still claim the old `1.0.1778378133` release. - [P1] Post-release verification must download published assets and inspect @@ -51,20 +51,20 @@ after the fix. ### T12.1 Pre-Tag Readiness - [ ] Confirm T11 is signed off by Elie in `tracker.md`. -- [ ] Confirm T9 recorded the exact `1.1.1778456247` version and all version files +- [ ] Confirm T9 recorded the exact `1.1.1778542197` version and all version files match it. - [ ] Confirm `CHANGELOG.md`, `LATEST_RELEASE.md`, and `docs/src/content/docs/releases/1-1.md` describe the actual shipped scope. - [ ] Confirm no active swarm agents or `In progress` finding docs remain. - [ ] Confirm `git status --short` has only intentional release files. -- [ ] Confirm tag `v1.1.1778456247` does not already exist locally or remotely. +- [ ] Confirm tag `v1.1.1778542197` does not already exist locally or remotely. ### T12.2 Tag and CI Run - [ ] If `just cut-release` has been updated to produce the exact T9 version, use it and record the tag it creates. - [ ] Otherwise, create the release commit and immutable tag manually, push the - branch and `v1.1.1778456247` tag, then run `just release v1.1.1778456247` to wait for CI. + branch and `v1.1.1778542197` tag, then run `just release v1.1.1778542197` to wait for CI. - [ ] Record the CI run URL in `tracker.md`. - [ ] Do not continue if any release job is skipped, allowed to fail, or marked neutral while it owns an expected release artifact. @@ -85,7 +85,7 @@ after the fix. ### T12.4 Live Release Asset Verification -- [ ] `gh release view v1.1.1778456247`. +- [ ] `gh release view v1.1.1778542197`. - [ ] Download published `manifest.json` and `manifest.json.minisig`. - [ ] Verify `manifest.json.minisig` with `config/manifest-sign.pub`. - [ ] Download published `.pkg`; run `pkgutil --check-signature`, @@ -103,7 +103,7 @@ after the fix. - [ ] Record final version, release URL, CI run URL, package asset names, manifest verification result, notarization/staple result, and clean-install proof in `tracker.md`. -- [ ] Confirm GitHub marks `v1.1.1778456247` as the latest release if intended. +- [ ] Confirm GitHub marks `v1.1.1778542197` as the latest release if intended. - [ ] Confirm docs/release-page links point to the landed version. - [ ] Mark this sprint complete only after CI is green and live assets verify. @@ -111,35 +111,63 @@ after the fix. | Category | Required proof | |---|---| -| Version | tag, changelog, binary metadata, release page, and release assets all say the exact `1.1.1778456247`. | +| Version | tag, changelog, binary metadata, release page, and release assets all say the exact `1.1.1778542197`. | | CI | every required release job is green and release-blocking. | | Artifacts | live manifest/signature, `.pkg`, `.deb`, and provenance assets are present and verified. | | Install | at least one downloaded package cleanly installs and runs an installed CLI/VM smoke. | | Release hygiene | immutable tag, release notes, latest-release metadata, and tracker evidence agree. | +## Landed Release Evidence + +- [x] GitHub latest release is `v1.1.1778542197` with `.pkg`, amd64/arm64 + `.deb`, signed `manifest.json`, signed `manifest.json.minisig`, SBOM, and + arm64/x86_64 boot assets. +- [x] Release workflow run `25703667428` completed successfully for + `release: v1.1.1778542197`; the macOS job notarized, stapled, and validated + the package, and `verify-release-downloads` exercised live GitHub downloads. +- [x] Main CI run `25723005949` and `Publish Site` run `25723006002` completed + successfully for the site/download follow-up merge `d2a1de9`. +- [x] Live `capsem.org/install.sh` downloads `manifest.json` and + `manifest.json.minisig`, verifies the manifest signature with minisign when + available, checks package SHA256 values from the manifest, and installs + `.pkg`/`.deb` artifacts with native installers. +- [x] Local release verification downloaded `manifest.json`, + `manifest.json.minisig`, `Capsem-1.1.1778542197.pkg`, + `Capsem_1.1.1778542197_amd64.deb`, and + `Capsem_1.1.1778542197_arm64.deb`; manifest minisign verification and all + package SHA256 checks matched the release metadata. +- [x] The hardened `scripts/verify_deb_payload.py` verifies both published + `.deb` packages, including zstd payloads without embedded content-size + headers. +- [x] Local macOS 26 package-assessment tools reported Code Signing subsystem + errors for both the v1.0 and v1.1 release packages while CI macOS 14 + notarization/stapling succeeded; the follow-up release workflow now makes + `pkgutil --check-signature` and `spctl -a -vv -t install` release-blocking + immediately after stapling. + ## Verification - [ ] `git status --short` - [ ] `git tag --list 'v1.1.*'` - [ ] `git ls-remote --tags origin 'v1.1.*'` -- [ ] `just release v1.1.1778456247` -- [ ] `gh release view v1.1.1778456247` -- [ ] `gh release download v1.1.1778456247 --pattern manifest.json -D /tmp/capsem-v1.1.1778456247` -- [ ] `gh release download v1.1.1778456247 --pattern manifest.json.minisig -D /tmp/capsem-v1.1.1778456247` -- [ ] `minisign -Vm /tmp/capsem-v1.1.1778456247/manifest.json -p config/manifest-sign.pub` -- [ ] `gh release download v1.1.1778456247 --pattern '*.pkg' -D /tmp/capsem-v1.1.1778456247` -- [ ] `pkgutil --check-signature /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `spctl -a -vv -t install /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `xcrun stapler validate /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `pkgutil --expand-full /tmp/capsem-v1.1.1778456247/Capsem-*.pkg /tmp/capsem-v1.1.1778456247/pkg-expanded` -- [ ] `gh release download v1.1.1778456247 --pattern '*.deb' -D /tmp/capsem-v1.1.1778456247` -- [ ] `dpkg-deb --contents /tmp/capsem-v1.1.1778456247/*.deb | rg 'manifest\\.json(\\.minisig)?|capsem-mcp-(aggregator|builtin)'` +- [ ] `just release v1.1.1778542197` +- [ ] `gh release view v1.1.1778542197` +- [ ] `gh release download v1.1.1778542197 --pattern manifest.json -D /tmp/capsem-v1.1.1778542197` +- [ ] `gh release download v1.1.1778542197 --pattern manifest.json.minisig -D /tmp/capsem-v1.1.1778542197` +- [ ] `minisign -Vm /tmp/capsem-v1.1.1778542197/manifest.json -p config/manifest-sign.pub` +- [ ] `gh release download v1.1.1778542197 --pattern '*.pkg' -D /tmp/capsem-v1.1.1778542197` +- [ ] `pkgutil --check-signature /tmp/capsem-v1.1.1778542197/Capsem-*.pkg` +- [ ] `spctl -a -vv -t install /tmp/capsem-v1.1.1778542197/Capsem-*.pkg` +- [ ] `xcrun stapler validate /tmp/capsem-v1.1.1778542197/Capsem-*.pkg` +- [ ] `pkgutil --expand-full /tmp/capsem-v1.1.1778542197/Capsem-*.pkg /tmp/capsem-v1.1.1778542197/pkg-expanded` +- [ ] `gh release download v1.1.1778542197 --pattern '*.deb' -D /tmp/capsem-v1.1.1778542197` +- [ ] `dpkg-deb --contents /tmp/capsem-v1.1.1778542197/*.deb | rg 'manifest\\.json(\\.minisig)?|capsem-mcp-(aggregator|builtin)'` ## Exit Criteria - [ ] T11 was signed off before the tag was pushed. -- [ ] CI is green for `v1.1.1778456247`. +- [ ] CI is green for `v1.1.1778542197`. - [ ] Published assets are present, signed, notarized where applicable, and payload-verified. - [ ] A downloaded package clean-install proof is recorded. -- [ ] The release is marked landed as `v1.1.1778456247` in `tracker.md`. +- [ ] The release is marked landed as `v1.1.1778542197` in `tracker.md`. diff --git a/sprints/release-policy-hardening/tracker.md b/sprints/release-policy-hardening/tracker.md index 4ac8f22..30fb874 100644 --- a/sprints/release-policy-hardening/tracker.md +++ b/sprints/release-policy-hardening/tracker.md @@ -2,7 +2,7 @@ ## Mission -Do not tag `v1.1.1778456247` until the release artifacts, Policy V2 UI/runtime, +Do not tag `v1.1.1778542197` until the release artifacts, Policy V2 UI/runtime, telemetry, docs, and package verification can prove the shipped behavior from a clean install. This tracker is the execution board; each `T*.md` file is the owning sub-sprint doc with detailed task lists. @@ -20,10 +20,10 @@ owning sub-sprint doc with detailed task lists. | T6 telemetry/session tooling | Implementation complete; focused real-session trace proof passed in T10 | P2 | Yes | Old/core DB compatibility, current Policy V2 schema checks, MCP correlation, timeline/triage layers, frontend policy fields, lifecycle tests, and legacy migration coverage are implemented. | | T7 swarm intake and review control | Owner mapping complete; downstream closeout open | P0 | Yes | FD01-FD14 transfer board, Galileo mapping audit, and command-validity sweep are captured; downstream blocker checkboxes stay open until T8-T12 resolves or defers each point. | | T8 policy integration E2E | Implementation complete; focused VM proof passed in T10 | P1 | Yes | Hook dispatch deferred for 1.1, backend/frontend hook writes rejected, reload banner dismissal and live `/settings` reload/timeline E2E path implemented; focused live `/settings` + `/reload-config` MCP E2E passed on 2026-05-10. | -| T9 release metadata and changelog | Implementation complete; commit discipline pending | P1 | Yes | Exact `1.1.1778456247` stamp, changelog, latest release, release page, lockfile, stamp recipe, and internal dependency metadata are synchronized. | +| T9 release metadata and changelog | Implementation complete; commit discipline pending | P1 | Yes | Exact `1.1.1778542197` stamp, changelog, latest release, release page, lockfile, stamp recipe, and internal dependency metadata are synchronized. | | T10 focused verification | Complete; T11 blockers explicit | P0 | Yes | Focused Rust/Python/frontend/docs, strict `.deb` install, host doctor, T8 policy E2E, Gate A visual proof, Gate B command proof, rootfs validation, frontend coverage, and `.pkg` expansion/signature proof are green or captured; clean installed-package proof and full-suite gates remain T11. | | T11 local release candidate gate | Full suite, private preflight, install smoke, installed doctor, demo UI, and final post-tray full gate green; manual sign-off open | P0 | Yes | Final `just test`, host doctor, `just exec "capsem-doctor"`, restored-private preflight, release workflow check, host package install, installed CLI run, installed doctor, rebuilt `.pkg` app-materialization fix, `/Applications` demo UI launch, `just run-ui --` process proof, and installed-app tray relaunch proof are captured. Elie Gate C/Gate D visual sign-off still blocks T12. | -| T12 CI green release landing | Not started | P0 | Yes | Tag `v1.1.1778456247`, wait for CI green, verify live release assets. | +| T12 CI green release landing | Release landed; CI hardening follow-up in progress | P0 | Yes | `v1.1.1778542197` is published/latest, release CI and site publish are green, live manifest/packages verify, and follow-up CI now blocks future releases on macOS pkg signature/Gatekeeper checks. | ## Active Swarm @@ -311,7 +311,7 @@ tests/capsem-session/test_check_session_compat.py -q` (2 passed), - [x] T8.1 Decide Shipping Scope. - [x] T8.2 If Hook Dispatch Ships: deferred post-1.1 because configured - external hook dispatch does not ship in `1.1.1778456247`. + external hook dispatch does not ship in `1.1.1778542197`. - [x] T8.3 If Hook Dispatch Does Not Ship. - [x] T8.4 Running Session Apply Semantics: implementation and UI tests done; focused VM proof passed in T10. @@ -473,7 +473,7 @@ tests/capsem-session/test_check_session_compat.py -q` (2 passed), - Telemetry: session/timeline assertion added to focused E2E path. - Performance: n/a. - Missing/deferred: configured external hook dispatch and image/fork selector - are deferred for `1.1.1778456247`; docs/UI mark or hide them. + are deferred for `1.1.1778542197`; docs/UI mark or hide them. - Runtime truth: T8.6 support matrix records shipped/deferred UI surfaces, and T2.8 proves or hides them. @@ -529,7 +529,7 @@ tests/capsem-session/test_check_session_compat.py -q` (2 passed), ### T12 -- Unit/contract: exact `1.1.1778456247` version agrees across tag, metadata, and +- Unit/contract: exact `1.1.1778542197` version agrees across tag, metadata, and release docs. - Functional: CI release workflow runs green and publishes expected assets. - Adversarial: release job fails if expected packages, manifests, signatures, @@ -620,13 +620,19 @@ tests/capsem-session/test_check_session_compat.py -q` (2 passed), - [x] `just build-ui` - [x] `just run-ui --` - [x] `open /Applications/Capsem.app` -- [ ] `just release v1.1.1778456247` -- [ ] `gh release view v1.1.1778456247` -- [ ] `minisign -Vm /tmp/capsem-v1.1.1778456247/manifest.json -p config/manifest-sign.pub` -- [ ] `pkgutil --check-signature /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `spctl -a -vv -t install /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `xcrun stapler validate /tmp/capsem-v1.1.1778456247/Capsem-*.pkg` -- [ ] `dpkg-deb --contents /tmp/capsem-v1.1.1778456247/*.deb | rg 'manifest\\.json(\\.minisig)?|capsem-mcp-(aggregator|builtin)'` +- [x] `gh release view v1.1.1778542197` +- [x] `gh run view 25703667428 --json status,conclusion,headSha,url,jobs` +- [x] `gh run view 25723005949 --json status,conclusion,headSha,url,jobs` +- [x] `gh run view 25723006002 --json status,conclusion,headSha,url,jobs` +- [x] `minisign -Vm /private/tmp/capsem-release-verify/manifest.json -x /private/tmp/capsem-release-verify/manifest.json.minisig -p config/manifest-sign.pub` +- [x] Package SHA256 checks for `Capsem-1.1.1778542197.pkg`, + `Capsem_1.1.1778542197_amd64.deb`, and + `Capsem_1.1.1778542197_arm64.deb` matched the release manifest. +- [x] `uv run --offline python scripts/verify_deb_payload.py /private/tmp/capsem-release-verify/Capsem_1.1.1778542197_amd64.deb /private/tmp/capsem-release-verify/Capsem_1.1.1778542197_arm64.deb --minisign-pubkey config/manifest-sign.pub` +- [ ] Local `pkgutil --check-signature`, `spctl -a -vv -t install`, and + `xcrun stapler validate` reported Code Signing subsystem errors on macOS 26 + for both v1.0 and v1.1 packages; follow-up release CI now makes + `pkgutil`/`spctl` package assessment release-blocking on macOS CI. ## Evidence Ledger @@ -641,7 +647,7 @@ here. Do not rely on chat history as evidence. | Gate B CLI/VM | Pass | `just exec "echo cli-ok"` + `just exec "capsem-doctor"` + T8 policy E2E | CLI output is understandable; VM doctor and policy/session proof pass | `sprints/release-policy-hardening/evidence/T10-2026-05-10-minisign-install-proof.md`; `sprints/release-policy-hardening/evidence/T11-2026-05-10-full-release-gate.md` | T10.5/T10.8/T11.4 | Final VM doctor rerun passed in T11; manual product sign-off still belongs to Gate C/Gate D. | | Gate C desktop dev launch | Partial | `just build-ui` + `just run-ui --` + browser screenshot | embedded UI launches, service/gateway reachable, and demo chrome does not show the build timestamp | `sprints/release-policy-hardening/evidence/T11-2026-05-10-full-release-gate.md`; `sprints/release-policy-hardening/evidence/T11-gate-c-no-build-stamp.png` | T11.4 | `just build-ui` and `just run-ui --` process proof passed; no-build-stamp screenshot is captured. Elie visual sign-off remains open until he accepts the installed app path. | | Gate D installed package | Partial | Fresh `.pkg` build + `just install` + installed service/CLI/UI smoke + Docker/systemd `.deb` install e2e inside `just test` | package payload contains signed manifest/signature/dev pubkey, every helper binary, and a postinstall `/Applications/Capsem.app` materialization fallback; Linux install e2e is green; host service responds; installed CLI VM run and installed doctor pass; demo UI process runs from `/Applications/Capsem.app`; killed tray is relaunched by the installed service when the app launches or remains open | `sprints/release-policy-hardening/evidence/T10-2026-05-10-minisign-install-proof.md`; `sprints/release-policy-hardening/evidence/T11-2026-05-10-full-release-gate.md`; `/private/tmp/capsem-pkg-1.1.1778445002-expanded` | T0/T10.1/T11.3 | Elie visual sign-off remains open before T12. | -| T12 CI/live release | Not run | `just release v1.1.1778456247` + `gh release view/download` | CI green, published assets signed/payload-verified, downloaded package proof recorded | TBD | TBD | TBD | +| T12 CI/live release | Pass with local macOS package-assessment caveat | `gh run view 25703667428`; `gh run view 25723005949`; `gh run view 25723006002`; `gh release view/download`; `minisign -Vm`; hardened `scripts/verify_deb_payload.py` | Release CI green, site publish green, published assets signed and SHA256-verified, downloaded `.deb` package payloads verified | `sprints/release-policy-hardening/T12-ci-release-landing.md#landed-release-evidence`; `/private/tmp/capsem-release-verify` | T12 | Local macOS 26 `pkgutil`/`spctl`/`stapler` reported Code Signing subsystem errors for both v1.0 and v1.1 packages; follow-up release CI now adds macOS pkg signature/Gatekeeper gates. | ## Pre-Sprint Transfer Ledger @@ -671,16 +677,16 @@ checkboxes in T7 stay open until implementation resolves or defers each point. - [x] Do we disable Tauri updater for this release, or build a full-install updater path that includes companion binaries and service/package state? - [x] Is configured external policy hook dispatch shipping in - `1.1.1778456247`, or is only Spec0/client/fail-closed/audit infrastructure + `1.1.1778542197`, or is only Spec0/client/fail-closed/audit infrastructure shipping? Decision: external dispatch is deferred; only Spec0/client/audit infrastructure plus non-hook Policy V2 enforcement ships. - [ ] Can clean macOS `.pkg` boot verification run in CI, or is it a manual release blocker with recorded proof? - [ ] Should helper/external stdio MCP children inherit any parent env beyond explicitly configured server env? -- [x] What exact `1.1.1778456247` suffix ships, and does `_stamp-version` need to +- [x] What exact `1.1.1778542197` suffix ships, and does `_stamp-version` need to change before `just install` or `just cut-release`? Decision: - `1.1.1778456247`; `_stamp-version` now defaults to `1.1.` and + `1.1.1778542197`; `_stamp-version` now defaults to `1.1.` and accepts `CAPSEM_RELEASE_VERSION` for exact stamping. ## Notes diff --git a/tests/test_release_workflow_policy.py b/tests/test_release_workflow_policy.py index c2edafd..38eaf84 100644 --- a/tests/test_release_workflow_policy.py +++ b/tests/test_release_workflow_policy.py @@ -213,6 +213,29 @@ def test_linux_deb_contents_validation_checks_each_required_payload(): assert payload in verifier +def test_macos_pkg_signature_and_gatekeeper_are_release_blocking(): + """Release macOS packages must be signed with Installer ID and assessed.""" + text = _workflow_text() + preflight = re.search( + r"(?ms)^ preflight:\n(?P.*?)(?=^ [a-zA-Z0-9_-]+:|\Z)", + text, + ) + assert preflight, "preflight job missing" + preflight_body = preflight.group("body") + assert "APPLE_INSTALLER_SIGNING_IDENTITY" in preflight_body + assert "Developer ID Installer:" in preflight_body + + build_macos = re.search( + r"(?ms)^ build-app-macos:\n(?P.*?)(?=^ [a-zA-Z0-9_-]+:|\Z)", + text, + ) + assert build_macos, "build-app-macos job missing" + body = build_macos.group("body") + assert '"$APPLE_INSTALLER_SIGNING_IDENTITY"' in body + assert "pkgutil --check-signature" in body + assert "spctl -a -vv -t install" in body + + def test_linux_app_manifest_signing_installs_minisign_before_use(): """Linux release app jobs must install minisign before signing manifests.""" text = _workflow_text() diff --git a/tests/test_verify_deb_payload.py b/tests/test_verify_deb_payload.py index 1635d48..1772860 100644 --- a/tests/test_verify_deb_payload.py +++ b/tests/test_verify_deb_payload.py @@ -9,6 +9,8 @@ from io import BytesIO from pathlib import Path +import zstandard + REPO_ROOT = Path(__file__).parent.parent VERIFY_SCRIPT = REPO_ROOT / "scripts" / "verify_deb_payload.py" @@ -34,6 +36,17 @@ def _tar_gz(files: dict[str, bytes]) -> bytes: return out.getvalue() +def _tar_zst_without_content_size(files: dict[str, bytes]) -> bytes: + out = BytesIO() + with tarfile.open(fileobj=out, mode="w:") as tar: + for name, data in files.items(): + info = tarfile.TarInfo(name) + info.size = len(data) + info.mode = 0o755 if name.startswith("./usr/bin/") else 0o644 + tar.addfile(info, BytesIO(data)) + return zstandard.ZstdCompressor(write_content_size=False).compress(out.getvalue()) + + def _ar_member(name: str, data: bytes) -> bytes: encoded_name = (name + "/").encode() header = ( @@ -55,6 +68,7 @@ def _write_deb( version: str = "1.1.0", architecture: str = "amd64", omit: str | None = None, + data_member: str = "data.tar.gz", ) -> None: control = _tar_gz({ "./control": ( @@ -69,12 +83,16 @@ def _write_deb( } payload_files["./usr/share/capsem/assets/manifest.json"] = b'{"format":2}\n' payload_files["./usr/share/capsem/assets/manifest.json.minisig"] = b"sig\n" - data = _tar_gz(payload_files) + data = ( + _tar_zst_without_content_size(payload_files) + if data_member == "data.tar.zst" + else _tar_gz(payload_files) + ) path.write_bytes( b"!\n" + _ar_member("debian-binary", b"2.0\n") + _ar_member("control.tar.gz", control) - + _ar_member("data.tar.gz", data) + + _ar_member(data_member, data) ) @@ -90,6 +108,18 @@ def test_verify_deb_accepts_required_payloads(tmp_path): ) +def test_verify_deb_accepts_zstd_payload_without_content_size(tmp_path): + deb = tmp_path / "Capsem_1.1.0_amd64.deb" + _write_deb(deb, data_member="data.tar.zst") + + verify_deb_payload.verify_deb( + deb, + expected_version="1.1.0", + expected_architecture="amd64", + minisign_pubkey=None, + ) + + def test_verify_deb_rejects_missing_required_payload(tmp_path): deb = tmp_path / "Capsem_1.1.0_amd64.deb" _write_deb(deb, omit="usr/bin/capsem-tray") diff --git a/uv.lock b/uv.lock index f26609a..dab4373 100644 --- a/uv.lock +++ b/uv.lock @@ -103,6 +103,7 @@ dependencies = [ { name = "click" }, { name = "jinja2" }, { name = "pydantic" }, + { name = "zstandard" }, ] [package.dev-dependencies] @@ -120,6 +121,7 @@ requires-dist = [ { name = "click", specifier = ">=8.0" }, { name = "jinja2", specifier = ">=3.0" }, { name = "pydantic", specifier = ">=2.0" }, + { name = "zstandard", specifier = ">=0.25.0" }, ] [package.metadata.requires-dev] @@ -703,3 +705,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]