From be0b0df12c4be0ff3f5ae5529d7a4a9ca918f1b2 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:55:07 +0000 Subject: [PATCH 01/25] Add disabled WASI GitOps canary packaging --- .github/workflows/build-wasi-oci.yml | 47 ++++++++++++++ Dockerfile.wasi | 8 +++ clusters/kopf-k3s/inspircd/configmap.yaml | 16 +++++ clusters/kopf-k3s/inspircd/deployment.yaml | 65 +++++++++++++++++++ clusters/kopf-k3s/inspircd/kustomization.yaml | 6 ++ clusters/kopf-k3s/inspircd/service.yaml | 17 +++++ 6 files changed, 159 insertions(+) create mode 100644 .github/workflows/build-wasi-oci.yml create mode 100644 Dockerfile.wasi create mode 100644 clusters/kopf-k3s/inspircd/configmap.yaml create mode 100644 clusters/kopf-k3s/inspircd/deployment.yaml create mode 100644 clusters/kopf-k3s/inspircd/kustomization.yaml create mode 100644 clusters/kopf-k3s/inspircd/service.yaml diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml new file mode 100644 index 0000000000..c930f58650 --- /dev/null +++ b/.github/workflows/build-wasi-oci.yml @@ -0,0 +1,47 @@ +name: Build WASI OCI artifact + +on: + push: + branches: [master] + paths: + - 'Dockerfile.wasi' + - '.github/workflows/build-wasi-oci.yml' + - 'CMakeLists.txt' + - 'include/**' + - 'modules/**' + - 'src/**' + - 'tools/wasi/**' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + package: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap digest-pinned tools + run: python3 tools/wasi/bootstrap.py + - name: Build the qualified command module + run: sh tools/wasi/build.sh + - name: Verify the WebAssembly module identity + run: | + set -eu + test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 + sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 + - name: Build OCI layout without publishing + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + with: + context: . + file: Dockerfile.wasi + outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + - name: Retain deployable artifact evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: inspircd-wasi-oci + path: | + /tmp/inspircd-wasi.oci.tar + build-wasi/inspircd.wasm.sha256 + retention-days: 7 diff --git a/Dockerfile.wasi b/Dockerfile.wasi new file mode 100644 index 0000000000..6cefcb9677 --- /dev/null +++ b/Dockerfile.wasi @@ -0,0 +1,8 @@ +# This image contains only the WASI command module. The host-selected runwasi +# handler supplies WasmEdge; do not add a native fallback runtime here. +FROM scratch + +COPY build-wasi/src/inspircd.wasm /inspircd.wasm + +ENTRYPOINT ["/inspircd.wasm"] +CMD ["--nofork", "--config", "/irc/inspircd.conf"] diff --git a/clusters/kopf-k3s/inspircd/configmap.yaml b/clusters/kopf-k3s/inspircd/configmap.yaml new file mode 100644 index 0000000000..29c034bc19 --- /dev/null +++ b/clusters/kopf-k3s/inspircd/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: inspircd-wasi-config + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment +data: + inspircd.conf: | + + + + + + diff --git a/clusters/kopf-k3s/inspircd/deployment.yaml b/clusters/kopf-k3s/inspircd/deployment.yaml new file mode 100644 index 0000000000..8333164f5e --- /dev/null +++ b/clusters/kopf-k3s/inspircd/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inspircd-wasi + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment + annotations: + inspircd.trycua.ai/status: "disabled pending a verified WasmEdge 0.17.2-rc.1 handler and immutable OCI digest" +spec: + # Keep the rendered canary inert until the runtime and OCI digest gates pass. + replicas: 0 + selector: + matchLabels: + app.kubernetes.io/name: inspircd-wasi + template: + metadata: + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment + spec: + runtimeClassName: wasmedge-cua + automountServiceAccountToken: false + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: inspircd + # Replaced only with an OCI digest and matching WASM SHA-256. + image: registry.invalid/trycua/inspircd@sha256:0000000000000000000000000000000000000000000000000000000000000000 + imagePullPolicy: IfNotPresent + args: ["--nofork", "--config", "/irc/inspircd.conf"] + ports: + - name: irc + containerPort: 6667 + protocol: TCP + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: runtime + mountPath: /irc + - name: config + mountPath: /irc/inspircd.conf + subPath: inspircd.conf + readOnly: true + volumes: + - name: runtime + emptyDir: {} + - name: config + configMap: + name: inspircd-wasi-config diff --git a/clusters/kopf-k3s/inspircd/kustomization.yaml b/clusters/kopf-k3s/inspircd/kustomization.yaml new file mode 100644 index 0000000000..a7ccd01f0f --- /dev/null +++ b/clusters/kopf-k3s/inspircd/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - configmap.yaml + - service.yaml + - deployment.yaml diff --git a/clusters/kopf-k3s/inspircd/service.yaml b/clusters/kopf-k3s/inspircd/service.yaml new file mode 100644 index 0000000000..2a4541c3a7 --- /dev/null +++ b/clusters/kopf-k3s/inspircd/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: inspircd-wasi + namespace: gvisor-dev + labels: + app.kubernetes.io/name: inspircd-wasi + app.kubernetes.io/part-of: inspircd-wasi-experiment +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: inspircd-wasi + ports: + - name: irc + port: 6667 + protocol: TCP + targetPort: irc From 80ab17a0332e68d4823d0899f20d0f23a41ccd48 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:59:18 +0000 Subject: [PATCH 02/25] Restrict disabled IRC canary to port forwarding --- clusters/kopf-k3s/inspircd/configmap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clusters/kopf-k3s/inspircd/configmap.yaml b/clusters/kopf-k3s/inspircd/configmap.yaml index 29c034bc19..99a51546c5 100644 --- a/clusters/kopf-k3s/inspircd/configmap.yaml +++ b/clusters/kopf-k3s/inspircd/configmap.yaml @@ -10,7 +10,7 @@ data: inspircd.conf: | - - + + From 19e7bb39d9cccb38dda5fb3ac551edeab899cffc Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:19:03 +0000 Subject: [PATCH 03/25] Publish WASI OCI artifacts to GHCR --- .github/workflows/build-wasi-oci.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index c930f58650..32c6b752c0 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -15,6 +15,7 @@ on: permissions: contents: read + packages: write jobs: package: @@ -32,12 +33,42 @@ jobs: test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 - name: Build OCI layout without publishing + if: github.event_name != 'push' || github.ref != 'refs/heads/master' uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: context: . file: Dockerfile.wasi outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + - name: Publish immutable OCI artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + id: publish + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + with: + context: . + file: Dockerfile.wasi + push: true + tags: ghcr.io/${{ github.repository_owner }}/inspircd:${{ github.sha }} + - name: Make this package public + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + await github.rest.packages.updatePackageForOrg({ + package_type: 'container', + package_name: 'inspircd', + org: context.repo.owner, + visibility: 'public', + }); + - name: Record immutable image digest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + set -eu + test -n '${{ steps.publish.outputs.digest }}' + printf 'image=ghcr.io/%s/inspircd@%s\n' '${{ github.repository_owner }}' '${{ steps.publish.outputs.digest }}' >> "$GITHUB_STEP_SUMMARY" + printf 'wasm_sha256=' >> "$GITHUB_STEP_SUMMARY" + cat build-wasi/inspircd.wasm.sha256 >> "$GITHUB_STEP_SUMMARY" - name: Retain deployable artifact evidence + if: github.event_name != 'push' || github.ref != 'refs/heads/master' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: inspircd-wasi-oci From daf692e535ab41425dd0a2e13dc5fd261736b7bd Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:36:14 +0000 Subject: [PATCH 04/25] Validate runwasi against pinned WasmEdge RC --- .github/workflows/ci-runwasi-rc.yml | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/ci-runwasi-rc.yml diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml new file mode 100644 index 0000000000..6aa0135cb7 --- /dev/null +++ b/.github/workflows/ci-runwasi-rc.yml @@ -0,0 +1,50 @@ +name: Validate runwasi against pinned WasmEdge RC + +on: + pull_request: + paths: + - '.github/workflows/ci-runwasi-rc.yml' + - 'tools/wasi/toolchain.json' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + build-shim: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap the checksum-pinned WasmEdge RC + run: python3 tools/wasi/bootstrap.py + - name: Fetch pinned runwasi source + run: | + set -eu + git init runwasi + git -C runwasi remote add origin https://github.com/containerd/runwasi.git + git -C runwasi fetch --depth=1 origin 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + git -C runwasi checkout --detach FETCH_HEAD + test "$(git -C runwasi rev-parse HEAD)" = 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + - name: Build dynamic shim against exactly the pinned RC + env: + WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools + LD_LIBRARY_PATH: ${{ github.workspace }}/.wasi-tools/lib64 + run: | + set -euxo pipefail + cargo --manifest-path runwasi/Cargo.toml build --release -p containerd-shim-wasmedge --no-default-features 2>&1 | tee runwasi-build.log + candidate=runwasi/target/release/containerd-shim-wasmedge-v1 + test -x "$candidate" + ldd "$candidate" | tee runwasi-ldd.log + grep -F 'libwasmedge.so.0.1.1' runwasi-ldd.log + - name: Retain shim build evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: runwasi-wasmedge-0.17.2-rc.1-evidence + path: | + runwasi-build.log + runwasi-ldd.log + runwasi/target/release/containerd-shim-wasmedge-v1 + if-no-files-found: warn + retention-days: 7 From 6b12fd8c50d58535fcab47ed1a03cb5f77e58f55 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:46:18 +0000 Subject: [PATCH 05/25] Fix runwasi CI manifest invocation --- .github/workflows/ci-runwasi-rc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 6aa0135cb7..bab84a4964 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -32,7 +32,7 @@ jobs: LD_LIBRARY_PATH: ${{ github.workspace }}/.wasi-tools/lib64 run: | set -euxo pipefail - cargo --manifest-path runwasi/Cargo.toml build --release -p containerd-shim-wasmedge --no-default-features 2>&1 | tee runwasi-build.log + cargo build --manifest-path runwasi/Cargo.toml --release -p containerd-shim-wasmedge --no-default-features 2>&1 | tee runwasi-build.log candidate=runwasi/target/release/containerd-shim-wasmedge-v1 test -x "$candidate" ldd "$candidate" | tee runwasi-ldd.log From 6068f118288cee1cfe50dbce164e17f5720fa1c3 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:54:49 +0000 Subject: [PATCH 06/25] Install runwasi protobuf build dependency --- .github/workflows/ci-runwasi-rc.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index bab84a4964..dcdd04978e 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -26,6 +26,11 @@ jobs: git -C runwasi fetch --depth=1 origin 5a8fc8e9ee0df28b0485da7419ee74c72127dfba git -C runwasi checkout --detach FETCH_HEAD test "$(git -C runwasi rev-parse HEAD)" = 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + - name: Install the native protobuf compiler required by runwasi + run: | + sudo apt-get update + sudo apt-get install --yes protobuf-compiler + protoc --version - name: Build dynamic shim against exactly the pinned RC env: WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools From 14506f699705cc5b4e8676a44981019d6ac6b0c6 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 05:10:45 +0000 Subject: [PATCH 07/25] Gate OCI publication on WASI acceptance --- .github/workflows/build-wasi-oci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index 32c6b752c0..57150e8a54 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -25,8 +25,12 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Bootstrap digest-pinned tools run: python3 tools/wasi/bootstrap.py - - name: Build the qualified command module - run: sh tools/wasi/build.sh + # Publish only the exact module that passed the full local WASI suite. + # verify.sh builds build-wasi/src/inspircd.wasm before exercising it. + - name: Build and fully validate the command module + env: + STRESS: '1' + run: sh tools/wasi/verify.sh - name: Verify the WebAssembly module identity run: | set -eu From 3a6245c770544ee24358ca7e11f374da10777a6c Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 05:14:17 +0000 Subject: [PATCH 08/25] Limit OCI publish credentials to protected job --- .github/workflows/build-wasi-oci.yml | 57 ++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index 57150e8a54..2e7805e879 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -11,14 +11,22 @@ on: - 'modules/**' - 'src/**' - 'tools/wasi/**' + pull_request: + paths: + - 'Dockerfile.wasi' + - '.github/workflows/build-wasi-oci.yml' + - 'CMakeLists.txt' + - 'include/**' + - 'modules/**' + - 'src/**' + - 'tools/wasi/**' workflow_dispatch: {} permissions: contents: read - packages: write jobs: - package: + validate: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: @@ -37,14 +45,44 @@ jobs: test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 - name: Build OCI layout without publishing - if: github.event_name != 'push' || github.ref != 'refs/heads/master' uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: context: . file: Dockerfile.wasi outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + - name: Retain deployable artifact evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: inspircd-wasi-oci + path: | + /tmp/inspircd-wasi.oci.tar + build-wasi/inspircd.wasm.sha256 + retention-days: 7 + + publish: + needs: validate + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + permissions: + contents: read + packages: write + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Bootstrap digest-pinned tools + run: python3 tools/wasi/bootstrap.py + # Revalidate in this privileged job so the image is built from the exact + # module that passed WASI acceptance immediately before publication. + - name: Build and fully validate the command module + env: + STRESS: '1' + run: sh tools/wasi/verify.sh + - name: Verify the WebAssembly module identity + run: | + set -eu + test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 + sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 - name: Publish immutable OCI artifact - if: github.event_name == 'push' && github.ref == 'refs/heads/master' id: publish uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: @@ -53,7 +91,6 @@ jobs: push: true tags: ghcr.io/${{ github.repository_owner }}/inspircd:${{ github.sha }} - name: Make this package public - if: github.event_name == 'push' && github.ref == 'refs/heads/master' uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | @@ -64,19 +101,9 @@ jobs: visibility: 'public', }); - name: Record immutable image digest - if: github.event_name == 'push' && github.ref == 'refs/heads/master' run: | set -eu test -n '${{ steps.publish.outputs.digest }}' printf 'image=ghcr.io/%s/inspircd@%s\n' '${{ github.repository_owner }}' '${{ steps.publish.outputs.digest }}' >> "$GITHUB_STEP_SUMMARY" printf 'wasm_sha256=' >> "$GITHUB_STEP_SUMMARY" cat build-wasi/inspircd.wasm.sha256 >> "$GITHUB_STEP_SUMMARY" - - name: Retain deployable artifact evidence - if: github.event_name != 'push' || github.ref != 'refs/heads/master' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: inspircd-wasi-oci - path: | - /tmp/inspircd-wasi.oci.tar - build-wasi/inspircd.wasm.sha256 - retention-days: 7 From 0e00501d1edaf7f6cb6a628bcd57558111577e24 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:51:23 +0000 Subject: [PATCH 09/25] Set up Buildx for OCI layout validation --- .github/workflows/build-wasi-oci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index 2e7805e879..41094304c4 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -44,6 +44,8 @@ jobs: set -eu test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build OCI layout without publishing uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: @@ -82,6 +84,8 @@ jobs: set -eu test "$(head -c 8 build-wasi/src/inspircd.wasm | od -An -t x1 | tr -d ' \n')" = 0061736d01000000 sha256sum build-wasi/src/inspircd.wasm | tee build-wasi/inspircd.wasm.sha256 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Publish immutable OCI artifact id: publish uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 From c4ae087ddaffc789b7caeefabb1a012c495a7cb0 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:59:08 +0000 Subject: [PATCH 10/25] Port WasmEdge bindings for RC runtime validation --- .github/workflows/ci-runwasi-rc.yml | 16 + tools/runtime/README.md | 9 + ...rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch | 296 ++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 tools/runtime/README.md create mode 100644 tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index dcdd04978e..d4df489965 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -5,6 +5,7 @@ on: paths: - '.github/workflows/ci-runwasi-rc.yml' - 'tools/wasi/toolchain.json' + - 'tools/runtime/**' workflow_dispatch: {} permissions: @@ -26,6 +27,21 @@ jobs: git -C runwasi fetch --depth=1 origin 5a8fc8e9ee0df28b0485da7419ee74c72127dfba git -C runwasi checkout --detach FETCH_HEAD test "$(git -C runwasi rev-parse HEAD)" = 5a8fc8e9ee0df28b0485da7419ee74c72127dfba + - name: Fetch and patch the official WasmEdge Rust SDK + run: | + set -euxo pipefail + git clone --depth=1 --branch 0.14.0 https://github.com/WasmEdge/WasmEdge-rust-sdk.git runwasi/wasmedge-rust-sdk + test "$(git -C runwasi/wasmedge-rust-sdk rev-parse HEAD)" = e7a74ab1837c43f609f26510cf3c456e9b9c2026 + git -C runwasi/wasmedge-rust-sdk apply "$GITHUB_WORKSPACE/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch" + python3 - <<'PY' + from pathlib import Path + path = Path("runwasi/crates/containerd-shim-wasmedge/Cargo.toml") + source = path.read_text() + old = 'wasmedge-sdk = { version = "0.14.0", default-features = false }' + new = 'wasmedge-sdk = { path = "../../wasmedge-rust-sdk", default-features = false }' + assert old in source + path.write_text(source.replace(old, new)) + PY - name: Install the native protobuf compiler required by runwasi run: | sudo apt-get update diff --git a/tools/runtime/README.md b/tools/runtime/README.md new file mode 100644 index 0000000000..275c915ecd --- /dev/null +++ b/tools/runtime/README.md @@ -0,0 +1,9 @@ +# Runtime Compatibility Experiments + +This directory holds reproducible, isolated compatibility work for the experimental RuntimeClass deployment path. It does not modify a cluster node or enable Flux. + +## WasmEdge Rust binding patch + +`patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch` applies to the official WasmEdge Rust SDK tag `0.14.0` at commit `e7a74ab1837c43f609f26510cf3c456e9b9c2026` (Apache-2.0). It adapts the C binding layer to the checksum-pinned WasmEdge `0.17.2-rc.1` headers selected by `tools/wasi/toolchain.json`. + +The patch preserves caller ownership of new `WasmEdge_LimitContext` values, destroys each temporary limit after the C type constructor returns, and uses checked conversions where the Rust SDK remains limited to wasm32 page counts. The CI workflow builds it only with pinned source and runtime inputs, then records its build and dynamic-link evidence. diff --git a/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch b/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch new file mode 100644 index 0000000000..1836425e3a --- /dev/null +++ b/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch @@ -0,0 +1,296 @@ +diff --git a/crates/wasmedge-sys/src/ast_module.rs b/crates/wasmedge-sys/src/ast_module.rs +index fd9f4f5..434d734 100644 +--- a/crates/wasmedge-sys/src/ast_module.rs ++++ b/crates/wasmedge-sys/src/ast_module.rs +@@ -181,7 +181,7 @@ impl ImportType<'_> { + )))), + false => { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(ctx_mem_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Memory(MemoryType::new( + limit.min(), +@@ -206,7 +206,7 @@ impl ImportType<'_> { + + // get the limit + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(ctx_tab_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Table(TableType::new( + elem_ty, +@@ -321,7 +321,7 @@ impl ExportType<'_> { + + // get the limit + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(ctx_tab_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Table(TableType::new( + elem_ty, +@@ -341,7 +341,7 @@ impl ExportType<'_> { + )))), + false => { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(ctx_mem_ty) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + + Ok(ExternalInstanceType::Memory(MemoryType::new( + limit.min(), +diff --git a/crates/wasmedge-sys/src/config.rs b/crates/wasmedge-sys/src/config.rs +index 5993ced..42143ac 100644 +--- a/crates/wasmedge-sys/src/config.rs ++++ b/crates/wasmedge-sys/src/config.rs +@@ -154,12 +154,12 @@ impl Config { + /// + /// * `count` - The page count (64KB per page). + pub fn set_max_memory_pages(&mut self, count: u32) { +- unsafe { ffi::WasmEdge_ConfigureSetMaxMemoryPage(self.inner.0, count) } ++ unsafe { ffi::WasmEdge_ConfigureSetMaxMemoryPage(self.inner.0, count.into()) } + } + + /// Returns the number of the memory pages available. + pub fn get_max_memory_pages(&self) -> u32 { +- unsafe { ffi::WasmEdge_ConfigureGetMaxMemoryPage(self.inner.0) } ++ unsafe { u32::try_from(ffi::WasmEdge_ConfigureGetMaxMemoryPage(self.inner.0)).expect("WasmEdge memory64 page count is not representable by this SDK API") } + } + + /// Enables or disables the ImportExportMutGlobals option. By default, the option is enabled. +diff --git a/crates/wasmedge-sys/src/instance/memory.rs b/crates/wasmedge-sys/src/instance/memory.rs +index ff76b6b..98fe86d 100644 +--- a/crates/wasmedge-sys/src/instance/memory.rs ++++ b/crates/wasmedge-sys/src/instance/memory.rs +@@ -73,8 +73,8 @@ impl Memory { + check(ffi::WasmEdge_MemoryInstanceGetData( + self.inner.0, + data.as_mut_ptr(), +- offset, +- len, ++ offset.into(), ++ len.into(), + ))?; + data.set_len(len as usize); + } +@@ -100,8 +100,8 @@ impl Memory { + check(ffi::WasmEdge_MemoryInstanceSetData( + self.inner.0, + data.as_ref().as_ptr(), +- offset, +- data.as_ref().len() as u32, ++ offset.into(), ++ data.as_ref().len() as u64, + )) + } + } +@@ -124,7 +124,7 @@ impl Memory { + /// The lifetime of the returned pointer must not exceed that of the object itself. + /// + pub unsafe fn data_pointer(&self, offset: u32, len: u32) -> WasmEdgeResult<*const u8> { +- let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointerConst(self.inner.0, offset, len) }; ++ let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointerConst(self.inner.0, offset.into(), len.into()) }; + match ptr.is_null() { + true => Err(Box::new(WasmEdgeError::Mem(MemError::ConstPtr))), + false => Ok(ptr), +@@ -148,7 +148,7 @@ impl Memory { + /// The lifetime of the returned pointer must not exceed that of the object itself. + /// + pub unsafe fn data_pointer_mut(&mut self, offset: u32, len: u32) -> WasmEdgeResult<*mut u8> { +- let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointer(self.inner.0, offset, len) }; ++ let ptr = unsafe { ffi::WasmEdge_MemoryInstanceGetPointer(self.inner.0, offset.into(), len.into()) }; + match ptr.is_null() { + true => Err(Box::new(WasmEdgeError::Mem(MemError::MutPtr))), + false => Ok(ptr), +@@ -157,7 +157,7 @@ impl Memory { + + /// Returns the size, in WebAssembly pages (64 KiB of each page), of this wasm memory. + pub fn size(&self) -> u32 { +- unsafe { ffi::WasmEdge_MemoryInstanceGetPageSize(self.inner.0) } ++ unsafe { u32::try_from(ffi::WasmEdge_MemoryInstanceGetPageSize(self.inner.0)).expect("WasmEdge memory64 page count is not representable by this SDK API") } + } + + /// Grows this WebAssembly memory by `count` pages. +@@ -171,7 +171,7 @@ impl Memory { + /// If fail to grow the page count, then an error is returned. + /// + pub fn grow(&mut self, count: u32) -> WasmEdgeResult<()> { +- unsafe { check(ffi::WasmEdge_MemoryInstanceGrowPage(self.inner.0, count)) } ++ unsafe { check(ffi::WasmEdge_MemoryInstanceGrowPage(self.inner.0, count.into())) } + } + + /// # Safety +@@ -264,8 +264,9 @@ impl MemType { + if shared && max.is_none() { + return Err(Box::new(WasmEdgeError::Mem(MemError::CreateSharedType))); + } +- let ctx = +- unsafe { ffi::WasmEdge_MemoryTypeCreate(WasmEdgeLimit::new(min, max, shared).into()) }; ++ let limit = WasmEdgeLimit::new(min, max, shared); ++ let limit_ctx = limit.as_context(); ++ let ctx = unsafe { ffi::WasmEdge_MemoryTypeCreate(limit_ctx.as_ptr()) }; + match ctx.is_null() { + true => Err(Box::new(WasmEdgeError::MemTypeCreate)), + false => Ok(Self { +@@ -277,21 +278,21 @@ impl MemType { + /// Returns the initial size of a [Memory]. + pub(crate) fn min(&self) -> u32 { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.min() + } + + /// Returns the maximum size of a [Memory] allowed to grow. + pub(crate) fn max(&self) -> Option { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.max() + } + + /// Returns whether the memory is shared or not. + pub(crate) fn shared(&self) -> bool { + let limit = unsafe { ffi::WasmEdge_MemoryTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.shared() + } + } +diff --git a/crates/wasmedge-sys/src/instance/table.rs b/crates/wasmedge-sys/src/instance/table.rs +index b3e6520..21f7497 100644 +--- a/crates/wasmedge-sys/src/instance/table.rs ++++ b/crates/wasmedge-sys/src/instance/table.rs +@@ -79,7 +79,7 @@ impl Table { + check(ffi::WasmEdge_TableInstanceGetData( + self.inner.0, + &mut data as *mut _, +- idx, ++ idx.into(), + ))?; + data + }; +@@ -102,7 +102,7 @@ impl Table { + check(ffi::WasmEdge_TableInstanceSetData( + self.inner.0, + data.as_raw(), +- idx, ++ idx.into(), + )) + } + } +@@ -125,7 +125,7 @@ impl Table { + /// + /// If fail to increase the size of the [Table], then an error is returned. + pub fn grow(&mut self, size: u32) -> WasmEdgeResult<()> { +- unsafe { check(ffi::WasmEdge_TableInstanceGrow(self.inner.0, size)) } ++ unsafe { check(ffi::WasmEdge_TableInstanceGrow(self.inner.0, size.into())) } + } + + /// # Safety +@@ -190,9 +190,9 @@ impl TableType { + /// + pub(crate) fn create(elem_ty: RefType, min: u32, max: Option) -> WasmEdgeResult { + let ty: ValType = elem_ty.into(); +- let ctx = unsafe { +- ffi::WasmEdge_TableTypeCreate(ty.into(), WasmEdgeLimit::new(min, max, false).into()) +- }; ++ let limit = WasmEdgeLimit::new(min, max, false); ++ let limit_ctx = limit.as_context(); ++ let ctx = unsafe { ffi::WasmEdge_TableTypeCreate(ty.into(), limit_ctx.as_ptr()) }; + if ctx.is_null() { + Err(Box::new(WasmEdgeError::TableTypeCreate)) + } else { +@@ -212,14 +212,14 @@ impl TableType { + /// Returns the initial size of the [Table]. + pub(crate) fn min(&self) -> u32 { + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.min() + } + + /// Returns the maximum size of the [Table]. + pub(crate) fn max(&self) -> Option { + let limit = unsafe { ffi::WasmEdge_TableTypeGetLimit(self.inner.0) }; +- let limit: WasmEdgeLimit = limit.into(); ++ let limit = unsafe { WasmEdgeLimit::from_context(limit) }; + limit.max() + } + } +diff --git a/crates/wasmedge-sys/src/types.rs b/crates/wasmedge-sys/src/types.rs +index 603ae56..bb6e724 100644 +--- a/crates/wasmedge-sys/src/types.rs ++++ b/crates/wasmedge-sys/src/types.rs +@@ -11,6 +11,7 @@ pub(crate) struct WasmEdgeLimit { + max: Option, + shared: bool, + } ++ + impl WasmEdgeLimit { + pub(crate) fn new(min: u32, max: Option, shared: bool) -> Self { + Self { min, max, shared } +@@ -27,26 +28,48 @@ impl WasmEdgeLimit { + pub(crate) fn shared(&self) -> bool { + self.shared + } ++ ++ // WasmEdge 0.17 owns limit contexts at the call site; type constructors ++ // borrow them, so retain this guard until the constructor returns. ++ pub(crate) fn as_context(&self) -> WasmEdgeLimitContext { ++ let ctx = unsafe { ++ match self.max { ++ Some(max) => ffi::WasmEdge_LimitCreateWithMax( ++ self.min.into(), max.into(), false, self.shared, ++ ), ++ None => ffi::WasmEdge_LimitCreate(self.min.into(), false), ++ } ++ }; ++ assert!(!ctx.is_null(), "WasmEdge failed to create a limit context"); ++ WasmEdgeLimitContext(ctx) ++ } ++ ++ pub(crate) unsafe fn from_context(ctx: *const ffi::WasmEdge_LimitContext) -> Self { ++ let min = u32::try_from(ffi::WasmEdge_LimitGetMin(ctx)) ++ .expect("WasmEdge memory64 limit is not representable by this SDK API"); ++ let max = if ffi::WasmEdge_LimitHasMax(ctx) { ++ Some( ++ u32::try_from(ffi::WasmEdge_LimitGetMax(ctx)) ++ .expect("WasmEdge memory64 limit is not representable by this SDK API"), ++ ) ++ } else { ++ None ++ }; ++ Self::new(min, max, ffi::WasmEdge_LimitIsShared(ctx)) ++ } + } +-impl From for ffi::WasmEdge_Limit { +- fn from(limit: WasmEdgeLimit) -> Self { +- let max = limit.max().unwrap_or(u32::MAX); + +- Self { +- Min: limit.min(), +- Max: max, +- HasMax: limit.max().is_some(), +- Shared: limit.shared, +- } ++pub(crate) struct WasmEdgeLimitContext(*mut ffi::WasmEdge_LimitContext); ++ ++impl WasmEdgeLimitContext { ++ pub(crate) fn as_ptr(&self) -> *const ffi::WasmEdge_LimitContext { ++ self.0 + } + } +-impl From for WasmEdgeLimit { +- fn from(limit: ffi::WasmEdge_Limit) -> Self { +- let max = match limit.HasMax { +- true => Some(limit.Max), +- false => None, +- }; +- WasmEdgeLimit::new(limit.Min, max, limit.Shared) ++ ++impl Drop for WasmEdgeLimitContext { ++ fn drop(&mut self) { ++ unsafe { ffi::WasmEdge_LimitDelete(self.0) } + } + } + From 447afccb4497639bd23d311d464ab68dde5d1dc0 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:01:32 +0000 Subject: [PATCH 11/25] Avoid nested SDK workspace in runtime validation --- .github/workflows/ci-runwasi-rc.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index d4df489965..9579f53e83 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -30,15 +30,15 @@ jobs: - name: Fetch and patch the official WasmEdge Rust SDK run: | set -euxo pipefail - git clone --depth=1 --branch 0.14.0 https://github.com/WasmEdge/WasmEdge-rust-sdk.git runwasi/wasmedge-rust-sdk - test "$(git -C runwasi/wasmedge-rust-sdk rev-parse HEAD)" = e7a74ab1837c43f609f26510cf3c456e9b9c2026 - git -C runwasi/wasmedge-rust-sdk apply "$GITHUB_WORKSPACE/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch" + git clone --depth=1 --branch 0.14.0 https://github.com/WasmEdge/WasmEdge-rust-sdk.git wasmedge-rust-sdk + test "$(git -C wasmedge-rust-sdk rev-parse HEAD)" = e7a74ab1837c43f609f26510cf3c456e9b9c2026 + git -C wasmedge-rust-sdk apply "$GITHUB_WORKSPACE/tools/runtime/patches/wasmedge-rust-sdk-0.14.0-wasmedge-0.17.2-rc.1.patch" python3 - <<'PY' from pathlib import Path path = Path("runwasi/crates/containerd-shim-wasmedge/Cargo.toml") source = path.read_text() old = 'wasmedge-sdk = { version = "0.14.0", default-features = false }' - new = 'wasmedge-sdk = { path = "../../wasmedge-rust-sdk", default-features = false }' + new = 'wasmedge-sdk = { path = "../../../wasmedge-rust-sdk", default-features = false }' assert old in source path.write_text(source.replace(old, new)) PY From 2c2d19436b414de0115b4099b18eccfe3b4d7307 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:06:43 +0000 Subject: [PATCH 12/25] Install runwasi seccomp link dependency --- .github/workflows/ci-runwasi-rc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 9579f53e83..f6d02df61e 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -45,7 +45,7 @@ jobs: - name: Install the native protobuf compiler required by runwasi run: | sudo apt-get update - sudo apt-get install --yes protobuf-compiler + sudo apt-get install --yes protobuf-compiler libseccomp-dev protoc --version - name: Build dynamic shim against exactly the pinned RC env: From d2ff0c6b3d6db171cefe4ad40b5ef38489a5f732 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:12:24 +0000 Subject: [PATCH 13/25] Verify shim resolves pinned RC library --- .github/workflows/ci-runwasi-rc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index f6d02df61e..c7eff8821d 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -57,7 +57,8 @@ jobs: candidate=runwasi/target/release/containerd-shim-wasmedge-v1 test -x "$candidate" ldd "$candidate" | tee runwasi-ldd.log - grep -F 'libwasmedge.so.0.1.1' runwasi-ldd.log + grep -F "$WASMEDGE_DIR/lib64/libwasmedge.so.0" runwasi-ldd.log + test "$(readlink -f "$WASMEDGE_DIR/lib64/libwasmedge.so.0")" = "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" - name: Retain shim build evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 From 2a0644e99401cc1baaa8c1131a5836cf237be2f3 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:20:59 +0000 Subject: [PATCH 14/25] Exercise InspIRCd through isolated runwasi containerd --- .github/workflows/ci-runwasi-rc.yml | 46 +++++++++++++++++++++- tools/runtime/containerd_irc_acceptance.py | 36 +++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tools/runtime/containerd_irc_acceptance.py diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index c7eff8821d..9589527c36 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -6,6 +6,8 @@ on: - '.github/workflows/ci-runwasi-rc.yml' - 'tools/wasi/toolchain.json' - 'tools/runtime/**' + - 'Dockerfile.wasi' + - 'clusters/kopf-k3s/inspircd/configmap.yaml' workflow_dispatch: {} permissions: @@ -14,7 +16,7 @@ permissions: jobs: build-shim: runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Bootstrap the checksum-pinned WasmEdge RC @@ -59,6 +61,45 @@ jobs: ldd "$candidate" | tee runwasi-ldd.log grep -F "$WASMEDGE_DIR/lib64/libwasmedge.so.0" runwasi-ldd.log test "$(readlink -f "$WASMEDGE_DIR/lib64/libwasmedge.so.0")" = "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" + - name: Build the validated InspIRCd OCI layout + env: + STRESS: '1' + run: | + set -euxo pipefail + sh tools/wasi/verify.sh + docker buildx build --file Dockerfile.wasi --output type=oci,dest=/tmp/inspircd-wasi.oci.tar . + sha256sum "$candidate" /tmp/inspircd-wasi.oci.tar | tee runtime-artifact.sha256 + - name: Install the isolated runtime handler into the runner containerd + env: + WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools + run: | + set -euxo pipefail + sudo install -m 0755 "$candidate" /usr/local/bin/containerd-shim-wasmedge-v1 + sudo install -m 0755 "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" /usr/local/lib/libwasmedge.so.0.1.1 + sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 + sudo ldconfig + sudo ctr version + sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar + - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients + run: | + set -euxo pipefail + mkdir -p /tmp/inspircd-irc + sed -n '/^ inspircd.conf: |$/,$p' clusters/kopf-k3s/inspircd/configmap.yaml | sed '1d;s/^ //' > /tmp/inspircd-irc/inspircd.conf + test -s /tmp/inspircd-irc/inspircd.conf + mkdir -p /tmp/inspircd-invalid + printf ' /tmp/inspircd-invalid/inspircd.conf + if timeout 20 sudo ctr run --rm --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-invalid,dst=/irc,options=rbind:rw local/inspircd:latest invalid-config; then + echo 'invalid config unexpectedly started' >&2 + exit 1 + fi + sudo ctr run --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-irc,dst=/irc,options=rbind:rw local/inspircd:latest inspircd-acceptance > inspircd-containerd.log 2>&1 & + runner=$! + trap 'sudo ctr tasks kill -s SIGKILL inspircd-acceptance 2>/dev/null || true; wait "$runner" || true' EXIT + for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + python3 tools/runtime/containerd_irc_acceptance.py | tee irc-acceptance.log + sudo ctr tasks kill -s SIGTERM inspircd-acceptance + wait "$runner" + trap - EXIT - name: Retain shim build evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -67,6 +108,9 @@ jobs: path: | runwasi-build.log runwasi-ldd.log + runtime-artifact.sha256 + inspircd-containerd.log + irc-acceptance.log runwasi/target/release/containerd-shim-wasmedge-v1 if-no-files-found: warn retention-days: 7 diff --git a/tools/runtime/containerd_irc_acceptance.py b/tools/runtime/containerd_irc_acceptance.py new file mode 100644 index 0000000000..d9555b3d31 --- /dev/null +++ b/tools/runtime/containerd_irc_acceptance.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import socket +import sys +import time + + +def recv_until(sock, marker, deadline=15): + end = time.monotonic() + deadline + data = b"" + while marker not in data: + if time.monotonic() >= end: + raise RuntimeError("did not receive %r; got %r" % (marker, data)) + sock.settimeout(max(0.1, end - time.monotonic())) + data += sock.recv(4096) + return data + + +def client(nick): + sock = socket.create_connection(("127.0.0.1", 6667), timeout=15) + sock.sendall(("NICK %s\r\nUSER %s 0 * :%s\r\n" % (nick, nick, nick)).encode()) + recv_until(sock, (" 001 %s " % nick).encode()) + sock.sendall(b"JOIN #wasmedge\r\n") + recv_until(sock, b"JOIN #wasmedge") + return sock + + +first = client("alpha") +second = client("beta") +first.sendall(b"PRIVMSG #wasmedge :alpha-to-beta\r\n") +recv_until(second, b"alpha-to-beta") +second.sendall(b"PRIVMSG #wasmedge :beta-to-alpha\r\n") +recv_until(first, b"beta-to-alpha") +for sock in (first, second): + sock.sendall(b"QUIT :integration-complete\r\n") + sock.close() +print("two-client IRC registration, join, and bidirectional messaging passed") From f7672c7046fb43469ba267eb8620d460a8d799cb Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:56:44 +0000 Subject: [PATCH 15/25] Enable OCI export in runtime integration CI --- .github/workflows/ci-runwasi-rc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 9589527c36..930de1cc02 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -61,6 +61,8 @@ jobs: ldd "$candidate" | tee runwasi-ldd.log grep -F "$WASMEDGE_DIR/lib64/libwasmedge.so.0" runwasi-ldd.log test "$(readlink -f "$WASMEDGE_DIR/lib64/libwasmedge.so.0")" = "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build the validated InspIRCd OCI layout env: STRESS: '1' From cf47dbcdadb5677ba980cbd662bbd7db6602014d Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:33:35 +0000 Subject: [PATCH 16/25] Share shim path across integration CI steps --- .github/workflows/ci-runwasi-rc.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 930de1cc02..150c5b988d 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -17,6 +17,8 @@ jobs: build-shim: runs-on: ubuntu-24.04 timeout-minutes: 45 + env: + CANDIDATE: runwasi/target/release/containerd-shim-wasmedge-v1 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Bootstrap the checksum-pinned WasmEdge RC @@ -56,9 +58,8 @@ jobs: run: | set -euxo pipefail cargo build --manifest-path runwasi/Cargo.toml --release -p containerd-shim-wasmedge --no-default-features 2>&1 | tee runwasi-build.log - candidate=runwasi/target/release/containerd-shim-wasmedge-v1 - test -x "$candidate" - ldd "$candidate" | tee runwasi-ldd.log + test -x "$CANDIDATE" + ldd "$CANDIDATE" | tee runwasi-ldd.log grep -F "$WASMEDGE_DIR/lib64/libwasmedge.so.0" runwasi-ldd.log test "$(readlink -f "$WASMEDGE_DIR/lib64/libwasmedge.so.0")" = "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" - name: Set up isolated Buildx builder @@ -70,13 +71,17 @@ jobs: set -euxo pipefail sh tools/wasi/verify.sh docker buildx build --file Dockerfile.wasi --output type=oci,dest=/tmp/inspircd-wasi.oci.tar . - sha256sum "$candidate" /tmp/inspircd-wasi.oci.tar | tee runtime-artifact.sha256 + test -x "$CANDIDATE" + test -s /tmp/inspircd-wasi.oci.tar + sha256sum "$CANDIDATE" /tmp/inspircd-wasi.oci.tar | tee runtime-artifact.sha256 - name: Install the isolated runtime handler into the runner containerd env: WASMEDGE_DIR: ${{ github.workspace }}/.wasi-tools run: | set -euxo pipefail - sudo install -m 0755 "$candidate" /usr/local/bin/containerd-shim-wasmedge-v1 + test -x "$CANDIDATE" + test -s /tmp/inspircd-wasi.oci.tar + sudo install -m 0755 "$CANDIDATE" /usr/local/bin/containerd-shim-wasmedge-v1 sudo install -m 0755 "$WASMEDGE_DIR/lib64/libwasmedge.so.0.1.1" /usr/local/lib/libwasmedge.so.0.1.1 sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 sudo ldconfig From b022d2cc6414d17c303d9a1c497f54b2a5332bbc Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 17:54:28 +0000 Subject: [PATCH 17/25] ci: tag imported OCI image for containerd acceptance --- .github/workflows/ci-runwasi-rc.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 150c5b988d..775347b62a 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -87,6 +87,10 @@ jobs: sudo ldconfig sudo ctr version sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar + imported_image="$(sudo ctr images list -q | awk '/^local\/inspircd@sha256:/ { print; exit }')" + test -n "$imported_image" + sudo ctr images tag "$imported_image" local/inspircd:latest + sudo ctr images list -q | grep -Fx local/inspircd:latest - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients run: | set -euxo pipefail From 8619b15cc623693f864c6a7fc52b10a20e4ffc81 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:14:37 +0000 Subject: [PATCH 18/25] ci: discover imported OCI image deterministically --- .github/workflows/ci-runwasi-rc.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 775347b62a..b235e42c2a 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -86,10 +86,11 @@ jobs: sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 sudo ldconfig sudo ctr version + sudo ctr images list -q | sort > /tmp/containerd-images-before-import sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar - imported_image="$(sudo ctr images list -q | awk '/^local\/inspircd@sha256:/ { print; exit }')" - test -n "$imported_image" - sudo ctr images tag "$imported_image" local/inspircd:latest + mapfile -t imported_images < <(comm -13 /tmp/containerd-images-before-import <(sudo ctr images list -q | sort)) + test "${#imported_images[@]}" -eq 1 + sudo ctr images tag "${imported_images[0]}" local/inspircd:latest sudo ctr images list -q | grep -Fx local/inspircd:latest - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients run: | From 7eb92a46303fb1789057461f686c6fe8f79326e5 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:42:12 +0000 Subject: [PATCH 19/25] ci: export a named OCI layout for containerd --- .github/workflows/build-wasi-oci.yml | 2 +- .github/workflows/ci-runwasi-rc.yml | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index 41094304c4..4bacb092e9 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -51,7 +51,7 @@ jobs: with: context: . file: Dockerfile.wasi - outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + outputs: type=oci,name=local/inspircd:latest,dest=/tmp/inspircd-wasi.oci.tar - name: Retain deployable artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index b235e42c2a..414ddb91c2 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -70,7 +70,18 @@ jobs: run: | set -euxo pipefail sh tools/wasi/verify.sh - docker buildx build --file Dockerfile.wasi --output type=oci,dest=/tmp/inspircd-wasi.oci.tar . + docker buildx build --file Dockerfile.wasi --output type=oci,name=local/inspircd:latest,dest=/tmp/inspircd-wasi.oci.tar . + python3 - /tmp/inspircd-wasi.oci.tar <<'PY' + import json + import tarfile + import sys + + with tarfile.open(sys.argv[1]) as archive: + index = json.load(archive.extractfile("index.json")) + manifests = index["manifests"] + assert len(manifests) == 1 + assert manifests[0]["annotations"]["org.opencontainers.image.ref.name"] == "local/inspircd:latest" + PY test -x "$CANDIDATE" test -s /tmp/inspircd-wasi.oci.tar sha256sum "$CANDIDATE" /tmp/inspircd-wasi.oci.tar | tee runtime-artifact.sha256 @@ -86,11 +97,7 @@ jobs: sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 sudo ldconfig sudo ctr version - sudo ctr images list -q | sort > /tmp/containerd-images-before-import - sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar - mapfile -t imported_images < <(comm -13 /tmp/containerd-images-before-import <(sudo ctr images list -q | sort)) - test "${#imported_images[@]}" -eq 1 - sudo ctr images tag "${imported_images[0]}" local/inspircd:latest + sudo ctr images import /tmp/inspircd-wasi.oci.tar sudo ctr images list -q | grep -Fx local/inspircd:latest - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients run: | From 02bc9048e089377da792fab1ec14738c27943cb6 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:09:25 +0000 Subject: [PATCH 20/25] ci: probe OCI layout naming before runtime acceptance --- .github/workflows/ci-oci-import-probe.yml | 102 ++++++++++++++++++++++ tools/runtime/oci-import-probe/Dockerfile | 3 + 2 files changed, 105 insertions(+) create mode 100644 .github/workflows/ci-oci-import-probe.yml create mode 100644 tools/runtime/oci-import-probe/Dockerfile diff --git a/.github/workflows/ci-oci-import-probe.yml b/.github/workflows/ci-oci-import-probe.yml new file mode 100644 index 0000000000..47969bf2c1 --- /dev/null +++ b/.github/workflows/ci-oci-import-probe.yml @@ -0,0 +1,102 @@ +name: Probe OCI import naming + +on: + pull_request: + paths: + - '.github/workflows/ci-oci-import-probe.yml' + - 'tools/runtime/oci-import-probe/**' + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Set up isolated Buildx builder + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Build small OCI layouts with documented name variants + run: | + set -euxo pipefail + docker buildx build --file tools/runtime/oci-import-probe/Dockerfile --output type=oci,dest=/tmp/unnamed.oci.tar tools/runtime/oci-import-probe + docker buildx build --file tools/runtime/oci-import-probe/Dockerfile --output type=oci,name=probe/inspircd:latest,dest=/tmp/named.oci.tar tools/runtime/oci-import-probe + - name: Add a spec-defined descriptor reference variant + run: | + set -euxo pipefail + python3 - <<'PY' + import io + import json + import tarfile + + source = "/tmp/unnamed.oci.tar" + destination = "/tmp/ref-name-latest.oci.tar" + with tarfile.open(source) as incoming, tarfile.open(destination, "w") as outgoing: + for member in incoming: + content = incoming.extractfile(member) if member.isfile() else None + if member.name == "index.json": + index = json.load(content) + descriptor = index["manifests"][0] + descriptor.setdefault("annotations", {})["org.opencontainers.image.ref.name"] = "latest" + payload = json.dumps(index, separators=(",", ":")).encode() + member.size = len(payload) + outgoing.addfile(member, io.BytesIO(payload)) + elif content is None: + outgoing.addfile(member) + else: + outgoing.addfile(member, content) + PY + - name: Inspect layouts and containerd import results + run: | + set -euxo pipefail + python3 - <<'PY' + import json + import subprocess + import tarfile + + reports = [] + for variant in ("unnamed", "named", "ref-name-latest"): + archive_path = f"/tmp/{variant}.oci.tar" + with tarfile.open(archive_path) as archive: + index = json.load(archive.extractfile("index.json")) + descriptor = index["manifests"][0] + blobs = sorted(member.name for member in archive if member.name.startswith("blobs/sha256/")) + namespace = f"oci-probe-{variant}" + imported = subprocess.run( + ["sudo", "ctr", "--namespace", namespace, "images", "import", "--base-name", "probe/inspircd", archive_path], + text=True, + capture_output=True, + ) + listed = subprocess.run( + ["sudo", "ctr", "--namespace", namespace, "images", "list", "-q"], + text=True, + check=True, + capture_output=True, + ) + reports.append({ + "variant": variant, + "descriptor_annotations": descriptor.get("annotations", {}), + "blob_digests": blobs, + "import_exit_code": imported.returncode, + "import_stdout": imported.stdout, + "import_stderr": imported.stderr, + "imported_images": listed.stdout.splitlines(), + }) + # Changing index.json must not alter any content-addressed image blob. + assert reports[0]["blob_digests"] == reports[2]["blob_digests"] + with open("oci-import-probe.json", "w") as report: + json.dump(reports, report, indent=2, sort_keys=True) + print(json.dumps(reports, indent=2, sort_keys=True)) + PY + - name: Retain OCI probe evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: oci-import-probe + path: | + oci-import-probe.json + /tmp/unnamed.oci.tar + /tmp/named.oci.tar + /tmp/ref-name-latest.oci.tar + retention-days: 7 diff --git a/tools/runtime/oci-import-probe/Dockerfile b/tools/runtime/oci-import-probe/Dockerfile new file mode 100644 index 0000000000..62cb469112 --- /dev/null +++ b/tools/runtime/oci-import-probe/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch + +LABEL org.opencontainers.image.title="inspircd-oci-import-probe" From 86edb8423545715d5a92ea8c84b10fddc3163c47 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:47 +0000 Subject: [PATCH 21/25] ci: normalize OCI references for containerd import --- .github/workflows/build-wasi-oci.yml | 6 ++- .github/workflows/ci-runwasi-rc.yml | 9 ++-- tools/runtime/normalize_oci_reference.py | 59 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tools/runtime/normalize_oci_reference.py diff --git a/.github/workflows/build-wasi-oci.yml b/.github/workflows/build-wasi-oci.yml index 4bacb092e9..d7c62b5c68 100644 --- a/.github/workflows/build-wasi-oci.yml +++ b/.github/workflows/build-wasi-oci.yml @@ -11,6 +11,7 @@ on: - 'modules/**' - 'src/**' - 'tools/wasi/**' + - 'tools/runtime/normalize_oci_reference.py' pull_request: paths: - 'Dockerfile.wasi' @@ -20,6 +21,7 @@ on: - 'modules/**' - 'src/**' - 'tools/wasi/**' + - 'tools/runtime/normalize_oci_reference.py' workflow_dispatch: {} permissions: @@ -51,7 +53,9 @@ jobs: with: context: . file: Dockerfile.wasi - outputs: type=oci,name=local/inspircd:latest,dest=/tmp/inspircd-wasi.oci.tar + outputs: type=oci,dest=/tmp/inspircd-wasi.oci.tar + - name: Normalize OCI import reference + run: python3 tools/runtime/normalize_oci_reference.py /tmp/inspircd-wasi.oci.tar - name: Retain deployable artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 414ddb91c2..700d27a802 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -70,7 +70,8 @@ jobs: run: | set -euxo pipefail sh tools/wasi/verify.sh - docker buildx build --file Dockerfile.wasi --output type=oci,name=local/inspircd:latest,dest=/tmp/inspircd-wasi.oci.tar . + docker buildx build --file Dockerfile.wasi --output type=oci,dest=/tmp/inspircd-wasi.oci.tar . + python3 tools/runtime/normalize_oci_reference.py /tmp/inspircd-wasi.oci.tar python3 - /tmp/inspircd-wasi.oci.tar <<'PY' import json import tarfile @@ -80,7 +81,9 @@ jobs: index = json.load(archive.extractfile("index.json")) manifests = index["manifests"] assert len(manifests) == 1 - assert manifests[0]["annotations"]["org.opencontainers.image.ref.name"] == "local/inspircd:latest" + annotations = manifests[0]["annotations"] + assert annotations["org.opencontainers.image.ref.name"] == "latest" + assert "io.containerd.image.name" not in annotations PY test -x "$CANDIDATE" test -s /tmp/inspircd-wasi.oci.tar @@ -97,7 +100,7 @@ jobs: sudo ln -sf libwasmedge.so.0.1.1 /usr/local/lib/libwasmedge.so.0 sudo ldconfig sudo ctr version - sudo ctr images import /tmp/inspircd-wasi.oci.tar + sudo ctr images import --base-name local/inspircd /tmp/inspircd-wasi.oci.tar sudo ctr images list -q | grep -Fx local/inspircd:latest - name: Exercise OCI lifecycle, invalid configuration, and two IRC clients run: | diff --git a/tools/runtime/normalize_oci_reference.py b/tools/runtime/normalize_oci_reference.py new file mode 100644 index 0000000000..8bd3c7c8bf --- /dev/null +++ b/tools/runtime/normalize_oci_reference.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Add the OCI reference metadata containerd needs when importing a layout.""" + +import argparse +import io +import json +import os +import tarfile +import tempfile +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("archive", type=Path) + parser.add_argument("--reference", default="latest") + args = parser.parse_args() + + with tarfile.open(args.archive) as incoming: + members = incoming.getmembers() + index_member = next((member for member in members if member.name == "index.json"), None) + if index_member is None: + raise SystemExit("OCI archive does not contain index.json") + index_stream = incoming.extractfile(index_member) + if index_stream is None: + raise SystemExit("OCI index.json is unreadable") + index = json.load(index_stream) + + manifests = index.get("manifests") + if not isinstance(manifests, list) or len(manifests) != 1: + raise SystemExit("OCI archive must have exactly one top-level manifest") + descriptor = manifests[0] + annotations = descriptor.setdefault("annotations", {}) + annotations["org.opencontainers.image.ref.name"] = args.reference + # BuildKit's containerd-private image-name annotation prevents ctr from + # materializing the OCI ref-name entry during import. + annotations.pop("io.containerd.image.name", None) + encoded_index = json.dumps(index, separators=(",", ":")).encode() + + with tempfile.NamedTemporaryFile(dir=args.archive.parent, delete=False) as temporary: + temporary_path = Path(temporary.name) + try: + with tarfile.open(args.archive) as incoming, tarfile.open(temporary_path, "w") as outgoing: + for member in incoming: + content = incoming.extractfile(member) if member.isfile() else None + if member.name == "index.json": + member.size = len(encoded_index) + outgoing.addfile(member, io.BytesIO(encoded_index)) + elif content is None: + outgoing.addfile(member) + else: + outgoing.addfile(member, content) + os.replace(temporary_path, args.archive) + finally: + temporary_path.unlink(missing_ok=True) + + +if __name__ == "__main__": + main() From 303b40e275e3b07bbba15b7886e31c036bb30ff5 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:34:05 +0000 Subject: [PATCH 22/25] test: match IRC JOIN message target syntax --- tools/runtime/containerd_irc_acceptance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/runtime/containerd_irc_acceptance.py b/tools/runtime/containerd_irc_acceptance.py index d9555b3d31..b1cb604cda 100644 --- a/tools/runtime/containerd_irc_acceptance.py +++ b/tools/runtime/containerd_irc_acceptance.py @@ -20,7 +20,7 @@ def client(nick): sock.sendall(("NICK %s\r\nUSER %s 0 * :%s\r\n" % (nick, nick, nick)).encode()) recv_until(sock, (" 001 %s " % nick).encode()) sock.sendall(b"JOIN #wasmedge\r\n") - recv_until(sock, b"JOIN #wasmedge") + recv_until(sock, b"JOIN :#wasmedge") return sock From 5def9be6dc7d6239ebba82a836126bcf8adde292 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:38:32 +0000 Subject: [PATCH 23/25] ci: bound non-interruptible WASI task termination --- .github/workflows/ci-runwasi-rc.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 700d27a802..9da270fe05 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -120,8 +120,31 @@ jobs: for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done python3 tools/runtime/containerd_irc_acceptance.py | tee irc-acceptance.log sudo ctr tasks kill -s SIGTERM inspircd-acceptance - wait "$runner" + if timeout 10 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$runner"; then + echo 'SIGTERM stopped the runtime task' | tee lifecycle.log + else + # WasmEdge guest execution is non-interruptible through this shim's + # signal path. Verify Kubernetes-equivalent grace expiry cleanup. + sudo ctr tasks list -q | grep -Fx inspircd-acceptance + echo 'SIGTERM grace period expired; forcing SIGKILL cleanup' | tee lifecycle.log + sudo ctr tasks kill -s SIGKILL inspircd-acceptance + timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$runner" + fi + wait "$runner" || true + trap - EXIT + ! sudo ctr tasks list -q | grep -Fx inspircd-acceptance + ! sudo ctr containers list -q | grep -Fx inspircd-acceptance + ! python3 -c 'import socket; socket.create_connection(("127.0.0.1", 6667), 1)' + sudo ctr run --rm --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-irc,dst=/irc,options=rbind:rw local/inspircd:latest inspircd-restart > inspircd-restart.log 2>&1 & + restart_runner=$! + trap 'sudo ctr tasks kill -s SIGKILL inspircd-restart 2>/dev/null || true; wait "$restart_runner" || true' EXIT + for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + sudo ctr tasks kill -s SIGKILL inspircd-restart + timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$restart_runner" + wait "$restart_runner" || true trap - EXIT + ! sudo ctr tasks list -q | grep -Fx inspircd-restart + ! sudo ctr containers list -q | grep -Fx inspircd-restart - name: Retain shim build evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -132,7 +155,9 @@ jobs: runwasi-ldd.log runtime-artifact.sha256 inspircd-containerd.log + inspircd-restart.log irc-acceptance.log + lifecycle.log runwasi/target/release/containerd-shim-wasmedge-v1 if-no-files-found: warn retention-days: 7 From 92a2f043a10c3ee4d20eaf4882f2d72d7736c332 Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:11:14 +0000 Subject: [PATCH 24/25] test: assert runtime readiness and cleanup state --- .github/workflows/ci-runwasi-rc.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 9da270fe05..2a337889cb 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -118,6 +118,7 @@ jobs: runner=$! trap 'sudo ctr tasks kill -s SIGKILL inspircd-acceptance 2>/dev/null || true; wait "$runner" || true' EXIT for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' python3 tools/runtime/containerd_irc_acceptance.py | tee irc-acceptance.log sudo ctr tasks kill -s SIGTERM inspircd-acceptance if timeout 10 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$runner"; then @@ -132,19 +133,25 @@ jobs: fi wait "$runner" || true trap - EXIT - ! sudo ctr tasks list -q | grep -Fx inspircd-acceptance - ! sudo ctr containers list -q | grep -Fx inspircd-acceptance + task_list="$(sudo ctr tasks list -q)" + ! printf '%s\n' "$task_list" | grep -Fx inspircd-acceptance + container_list="$(sudo ctr containers list -q)" + ! printf '%s\n' "$container_list" | grep -Fx inspircd-acceptance ! python3 -c 'import socket; socket.create_connection(("127.0.0.1", 6667), 1)' sudo ctr run --rm --net-host --runtime=io.containerd.wasmedge.v1 --mount type=bind,src=/tmp/inspircd-irc,dst=/irc,options=rbind:rw local/inspircd:latest inspircd-restart > inspircd-restart.log 2>&1 & restart_runner=$! trap 'sudo ctr tasks kill -s SIGKILL inspircd-restart 2>/dev/null || true; wait "$restart_runner" || true' EXIT for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done + python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' sudo ctr tasks kill -s SIGKILL inspircd-restart timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$restart_runner" wait "$restart_runner" || true trap - EXIT - ! sudo ctr tasks list -q | grep -Fx inspircd-restart - ! sudo ctr containers list -q | grep -Fx inspircd-restart + task_list="$(sudo ctr tasks list -q)" + ! printf '%s\n' "$task_list" | grep -Fx inspircd-restart + container_list="$(sudo ctr containers list -q)" + ! printf '%s\n' "$container_list" | grep -Fx inspircd-restart + ! python3 -c 'import socket; socket.create_connection(("127.0.0.1", 6667), 1)' - name: Retain shim build evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 From bb0b8f77ca683f1fb550b2b1e345fb2b1729a63f Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:14:28 +0000 Subject: [PATCH 25/25] test: verify IRC exchange after runtime restart --- .github/workflows/ci-runwasi-rc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-runwasi-rc.yml b/.github/workflows/ci-runwasi-rc.yml index 2a337889cb..e6327e3d1e 100644 --- a/.github/workflows/ci-runwasi-rc.yml +++ b/.github/workflows/ci-runwasi-rc.yml @@ -125,7 +125,7 @@ jobs: echo 'SIGTERM stopped the runtime task' | tee lifecycle.log else # WasmEdge guest execution is non-interruptible through this shim's - # signal path. Verify Kubernetes-equivalent grace expiry cleanup. + # signal path. Bound this local ctr cleanup and record forced stop. sudo ctr tasks list -q | grep -Fx inspircd-acceptance echo 'SIGTERM grace period expired; forcing SIGKILL cleanup' | tee lifecycle.log sudo ctr tasks kill -s SIGKILL inspircd-acceptance @@ -143,6 +143,7 @@ jobs: trap 'sudo ctr tasks kill -s SIGKILL inspircd-restart 2>/dev/null || true; wait "$restart_runner" || true' EXIT for _ in $(seq 1 30); do python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' && break || sleep 1; done python3 -c 'import socket; s=socket.create_connection(("127.0.0.1",6667),1); s.close()' + python3 tools/runtime/containerd_irc_acceptance.py | tee -a irc-acceptance.log sudo ctr tasks kill -s SIGKILL inspircd-restart timeout 15 bash -c 'while kill -0 "$1" 2>/dev/null; do sleep 0.2; done' _ "$restart_runner" wait "$restart_runner" || true