diff --git a/.github/workflows/gustos-publish.yml b/.github/workflows/gustos-publish.yml new file mode 100644 index 0000000..ff768dd --- /dev/null +++ b/.github/workflows/gustos-publish.yml @@ -0,0 +1,118 @@ +# Build, verify, sign + publish gale's gust:os provider components +# (DD-OS-DELIVERY-001, gale#223/#224). +# +# gust's OS-as-wasm-component-model made distributable: each gust:os provider is a +# wasm COMPONENT that exports a gust:os capability interface and imports only +# gust:hal. The `verify` job gates that invariant on every PR (export gust:os, +# import only gust:hal — no leaking scheduler/env/heap). On a release tag the +# `publish` job additionally cosign-signs (keyless OIDC) and publishes to BOTH +# channels a consumer pulls from — a cosign-signed OCI registry (ghcr) and +# wasm.directory — so ANY downstream that imports gust:os pulls the same signed +# component and fuses it for its target. +# +# First demonstrator: the time provider (exports gust:os/time, residual = gust:hal). +# The full fused gale-nano runtime component is gale#224. +name: gustos publish + +on: + push: + branches: [main] + paths: + - "benches/gust/drivers/*-provider/**" + - "benches/gust/drivers/wit-os/**" + - "benches/gust/drivers/build-gustos-components.sh" + - ".github/workflows/gustos-publish.yml" + tags: ["v*"] + pull_request: + branches: [main] + paths: + - "benches/gust/drivers/*-provider/**" + - "benches/gust/drivers/wit-os/**" + - "benches/gust/drivers/build-gustos-components.sh" + - ".github/workflows/gustos-publish.yml" + workflow_dispatch: + inputs: + version: + description: "component version tag (e.g. 0.5.1)" + required: true + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + verify: + name: "gust:os component invariant (export gust:os, import only gust:hal)" + runs-on: ubuntu-22.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - name: Install wasm-tools + run: cargo install wasm-tools --version 1.245.1 --locked + - name: Build + verify the gust:os provider components + run: bash benches/gust/drivers/build-gustos-components.sh + + publish: + name: "sign + publish gust:os components (OCI + wasm.directory)" + # Only on a release tag or an explicit dispatch — never on PR. + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + needs: verify + runs-on: ubuntu-22.04 + timeout-minutes: 20 + permissions: + contents: read + packages: write # ghcr OCI push + id-token: write # cosign keyless OIDC + steps: + - uses: actions/checkout@v7 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - name: Install wasm-tools + wkg + run: | + cargo install wasm-tools --version 1.245.1 --locked + cargo install wkg --locked + - uses: sigstore/cosign-installer@v3 + with: + cosign-release: 'v2.4.1' + - name: Build + verify components + run: bash benches/gust/drivers/build-gustos-components.sh + - name: Resolve version + id: v + env: + INPUT_VERSION: ${{ github.event.inputs.version }} + REF: ${{ github.ref }} + run: | + if [ -n "$INPUT_VERSION" ]; then V="$INPUT_VERSION"; else V="${REF#refs/tags/v}"; fi + echo "version=$V" >> "$GITHUB_OUTPUT" + - name: Publish signed to ghcr OCI + wasm.directory + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WASM_DIRECTORY_TOKEN: ${{ secrets.WASM_DIRECTORY_TOKEN }} + VERSION: ${{ steps.v.outputs.version }} + OWNER: ${{ github.repository_owner }} + run: | + set -euo pipefail + echo "$GH_TOKEN" | wkg oci login ghcr.io -u "$OWNER" --password-stdin + for comp in benches/gust/drivers/gustos-components/*.component.wasm; do + base="$(basename "$comp" .component.wasm)" # time-provider + pkg="gale-nano-${base%-provider}" # gale-nano-time + ref="ghcr.io/${OWNER}/${pkg}:${VERSION}" + echo "== $pkg → $ref ==" + wkg oci push "$ref" "$comp" + cosign sign --yes "$ref" + echo "::notice::published + signed $ref" + if [ -n "${WASM_DIRECTORY_TOKEN:-}" ]; then + WKG_REGISTRY_TOKEN="$WASM_DIRECTORY_TOKEN" \ + wkg publish "$comp" --package "pulseengine:${pkg}@${VERSION}" --registry wasm.directory \ + || echo "::warning::wasm.directory publish failed for $pkg" + else + echo "::warning::WASM_DIRECTORY_TOKEN unset — skipping wasm.directory publish for $pkg" + fi + done diff --git a/.gitignore b/.gitignore index 99a10ba..86e4f72 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ MODULE.bazel.lock # Agent working state (per-package; not source — see issue-hunt skill) .claude/pulseengine/ **/.claude/pulseengine/ +benches/gust/drivers/gustos-components/ diff --git a/benches/gust/drivers/build-gustos-components.sh b/benches/gust/drivers/build-gustos-components.sh new file mode 100755 index 0000000..7d437c8 --- /dev/null +++ b/benches/gust/drivers/build-gustos-components.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Build gale's gust:os PROVIDER components — the consumable, publishable unit of +# gust's OS-as-wasm-component-model (DD-OS-DELIVERY-001, gale#223/#224). +# +# Each provider is a wasm COMPONENT that EXPORTS a gust:os capability interface and +# imports ONLY the gust:hal seam — the exact delivery invariant: a downstream that +# imports gust:os pulls the signed component and fuses it for its target; the only +# native residual is gust:hal. This is the first, minimal demonstrator (the time +# provider) ahead of the full fused gale-nano runtime component (gale#224). +# +# bash benches/gust/drivers/build-gustos-components.sh # build + verify +# OUT=/some/dir bash .../build-gustos-components.sh # choose output dir +# +# Verifies (the oracle for the DD invariant): every built component EXPORTS at least +# one gust:os/* interface and imports NOTHING outside gust:hal/*. A leaking scheduler +# / env / heap import (the core-module shape the release tarball ships) fails here. +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +OUT="${OUT:-$HERE/gustos-components}" +WT="${WASM_TOOLS:-wasm-tools}" +mkdir -p "$OUT" + +# gust:os provider crates → the wasm cdylib each emits. Start with the time provider +# (0-SRAM, exports gust:os/time); extend to log/spawn/timer as they harden into the +# published set (the full fused runtime is gale#224). +PROVIDERS=( + "time-provider:gust_time_provider" +) + +fail="" +for entry in "${PROVIDERS[@]}"; do + crate="${entry%%:*}"; wasm_name="${entry##*:}" + printf '== %s ==\n' "$crate" + ( cd "$HERE/$crate" && cargo build --release --target wasm32-unknown-unknown >/dev/null 2>&1 ) + core="$(find "$HERE/$crate/target/wasm32-unknown-unknown/release" -maxdepth 1 -name "$wasm_name.wasm" | head -1)" + comp="$OUT/$crate.component.wasm" + "$WT" component new "$core" -o "$comp" + + wit="$("$WT" component wit "$comp")" + exports_gustos="$(printf '%s' "$wit" | grep -cE '^\s*export gust:os/' || true)" + # imports outside gust:hal/* (the leak check): any `import X` whose target isn't gust:hal/ + bad_imports="$(printf '%s' "$wit" | grep -E '^\s*import ' | grep -vE 'import gust:hal/' || true)" + + if [ "$exports_gustos" -lt 1 ]; then + echo " FAIL: exports no gust:os/* interface"; fail="$fail $crate" + elif [ -n "$bad_imports" ]; then + echo " FAIL: imports outside gust:hal:"; printf '%s\n' "$bad_imports" | sed 's/^/ /'; fail="$fail $crate" + else + echo " ok: exports gust:os ($exports_gustos iface), residual import = gust:hal only → $comp" + fi +done + +if [ -n "$fail" ]; then + echo ""; echo "gust:os component invariant FAILED:$fail"; exit 1 +fi +echo "" +echo "gust:os provider components built + invariant held (export gust:os, import only gust:hal). Output: $OUT"