diff --git a/.dockerignore b/.dockerignore index 9550d54..9ecc731 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ # Git metadata is not part of the image build. Component versions come from -# their source manifests, and exact revisions remain traceable through the -# parent repository's submodule gitlinks. +# source manifests or pinned releases; revisions remain traceable through +# submodule gitlinks and packaged release provenance. .git **/.git @@ -20,9 +20,9 @@ deploy/standalone/data/ deploy/standalone/logs/ deploy/standalone/output/ -# Local component build outputs. The image build compiles these components in -# dedicated Docker stages. -src/distill-fs/target/ +# Local source build outputs. +# distill-fs is consumed as a release artifact, not from this checkout. +src/distill-fs/ src/sandboxd/output/ src/yuanrong/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c64cba..c88c17a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: - name: Initialize build submodules run: | - git submodule update --init src/sandboxd src/distill-fs + git submodule update --init src/sandboxd - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -210,6 +210,18 @@ jobs: echo "=== akernel-node: service journal ===" docker exec akernel-node journalctl --no-pager -n 500 \ -u sandboxd.service -u yuanrong.service || true + docker exec akernel-node bash -c ' + shopt -s nullglob + for logfile in \ + /home/akernel/logs/sandboxd/sandboxd*.log \ + /home/yuanrong/logs/*function_master*.log \ + /home/yuanrong/logs/*function_proxy*.log \ + /home/yuanrong/logs/*function_agent*.log \ + /home/akernel/sandboxd/image_manager/daemons/*/daemon.log; do + echo "=== ${logfile} ===" + tail -n 300 "${logfile}" + done + ' || true fi - name: Stop standalone AKernel diff --git a/AGENTS.md b/AGENTS.md index 2c72a0e..1de3944 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,8 +48,11 @@ tunnels. The project overview and deployment quick start are in The open-source AKernel repository contains the SDK, deployment configuration, build tooling, and examples. Node runtime components such as `sandboxd` and `distill-fs` are maintained in their own upstream repositories and pinned as -Git submodules. The all-in-one build compiles those revisions and packages the -runtime payloads described in the Build section below. +Git submodules. The all-in-one build compiles sandboxd and downloads the +checksum-pinned static distill-fs release recorded in +`builder/distill-fs-versions.env`. The distill-fs submodule is an optional +source reference, not a build input. See the Build section below for runtime +payloads. ## Common Commands @@ -129,9 +132,9 @@ actor backend is deprecated and retained only for compatibility with existing applications. Keep it on its explicitly pinned legacy version; do not advance it with the default `openyuanrong-sandbox` backend or use it for new features. -Initialize submodules with `git submodule update --init --recursive` before +Initialize sandboxd with `git submodule update --init src/sandboxd` before building. The all-in-one image builds the sandboxd binaries, including -`firecracker-agent`, and `distill_fs`; installs checksum-pinned gVisor and Kata +`firecracker-agent`; installs checksum-pinned static distill-fs, gVisor, and Kata artifacts; installs the Firecracker VMM and guest kernel; and constructs the matching guest-agent initrd. Runc remains build-time optional, and `AKERNEL_ENABLE_FIRECRACKER=false` excludes the Firecracker payload. @@ -149,14 +152,24 @@ pins it rather than overriding manifest fields from the AKernel build. Keep sandboxd's pooled-TAP contract and the matching gVisor compatibility patches validated together when upgrading. -The submodule gitlinks are the single source of truth for the sandboxd and -distill-fs revisions included in a clean release. `make build` always compiles -the local submodule worktrees, so developers may check out a different commit -or edit either directory and rebuild without pushing first. Each component -maintains and embeds its own semantic version: sandboxd uses -`version/VERSION`, while distill-fs uses the package version in `Cargo.toml`. -AKernel does not inject parent-repository version metadata into component -compilation. +The sandboxd gitlink fixes the source revision compiled by `make build`. +AKernel's `builder/distill-fs-versions.env` fixes the distill-fs release URL +and SHA-256. `make build` compiles the local sandboxd worktree and consumes the +static distill-fs release through `builder/scripts/install-distill-fs.sh`; editing +`src/distill-fs` no longer affects the image. The installer verifies the archive, +provenance, version, binary hash, and static ELF contract, and packages its +licenses and manifest under `/usr/local/share/distill-fs`. + +Publish and verify a distill-fs release before updating the AKernel manifest +pin. This dependency does not require a sandboxd source or gitlink change. +Never use a guessed checksum or silently fall back to a source build. +Missing or invalid release pins prevent builds. `make versions` reports the +release tag and archive digest without requiring the distill-fs submodule. + +Each component embeds its own semantic version: sandboxd uses +`version/VERSION`, while distill-fs uses its release package version in +`Cargo.toml`. AKernel does not inject parent-repository version metadata into +component compilation. To test an unreleased openYuanRong core wheel without rebuilding YuanRong, provide both `OPEN_YR_CORE_WHEEL_URL` and `OPEN_YR_CORE_WHEEL_SHA256` to @@ -175,7 +188,8 @@ make versions The final image uses standard OCI labels for the AKernel version and revision. Component semantic versions are reported by their binaries, and their exact -source revisions are traceable through the AKernel commit's submodule gitlinks. +source revisions are traceable through the sandboxd gitlink and the pinned +distill-fs release's packaged manifest. ## Deploy diff --git a/README.md b/README.md index 2f54892..314e5e3 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ See the complete [basic usage example](./sdk/python/examples/basic_usage.py), th - **Sandbox runtimes**: gVisor by default; Kata Containers and Firecracker on KVM-capable nodes; and an explicitly enabled native Linux runc backend - **sandboxd**: Sandbox lifecycle daemon with pluggable sandbox runtime integration -- **distill-fs**: Rust-based FUSE filesystem for lazy rootfs access, chunk caching, and deduplication +- **distill-fs**: Rust-based FUSE filesystem for lazy rootfs access, chunk caching, and deduplication; packaged from a static GitHub Release with its version and checksum pinned in AKernel **Cluster-Wide Services** - **Distributed Scheduler**: Workload-aware placement and scaling diff --git a/builder/distill-fs-versions.env b/builder/distill-fs-versions.env new file mode 100644 index 0000000..2ef24c2 --- /dev/null +++ b/builder/distill-fs-versions.env @@ -0,0 +1,8 @@ +# Copyright (c) 2026 Ant Group Corporation. +# SPDX-License-Identifier: Apache-2.0 + +# AKernel's distill-fs release dependency. Verify the published release archive +# before updating its version, URL, and SHA-256 together. +DISTILL_FS_RELEASE=v0.1.1 +DISTILL_FS_AMD64_URL=https://github.com/inclusionAI/distill-fs/releases/download/v0.1.1/distill-fs-v0.1.1-linux-amd64.tar.gz +DISTILL_FS_AMD64_SHA256=b16e225e5b777f673bd98dd0968a15c190e333690356552b69a2b2624c3d5dcb diff --git a/builder/node.Dockerfile b/builder/node.Dockerfile index 77ba145..f7b089f 100644 --- a/builder/node.Dockerfile +++ b/builder/node.Dockerfile @@ -9,7 +9,6 @@ ARG AKERNEL_ENABLE_KATA=true ARG AKERNEL_ENABLE_RUNC=false ARG AKERNEL_ENABLE_FIRECRACKER=true ARG SANDBOXD_BUILD_IMAGE=golang:1.25.5-bookworm -ARG DISTILL_FS_BUILD_IMAGE=rust:1.85.0-bookworm ARG OPEN_YR_VERSION=0.10.2rc2 ARG OPEN_YR_CORE_WHEEL_URL= ARG OPEN_YR_CORE_WHEEL_SHA256= @@ -214,23 +213,17 @@ RUN mkdir -p /runc/usr/local/bin FROM runc-runtime-${AKERNEL_ENABLE_RUNC} AS runc-runtime -FROM ${DISTILL_FS_BUILD_IMAGE} AS distill-fs-builder -ENV DEBIAN_FRONTEND=noninteractive \ - CARGO_NET_GIT_FETCH_WITH_CLI=true +FROM ubuntu:24.04 AS distill-fs-runtime +ARG TARGETARCH +ARG DISTILL_FS_RELEASE +ARG DISTILL_FS_AMD64_URL +ARG DISTILL_FS_AMD64_SHA256 RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - cmake \ - g++ \ - gcc \ - git \ - make \ - perl \ - pkg-config && \ + apt-get install -y --no-install-recommends ca-certificates curl jq binutils && \ rm -rf /var/lib/apt/lists/* -WORKDIR /src/distill-fs -COPY ./src/distill-fs/ ./ -RUN cargo build --locked --release --bin distill_fs +COPY ./builder/scripts/install-distill-fs.sh /install-distill-fs.sh +RUN sh /install-distill-fs.sh "$DISTILL_FS_RELEASE" \ + "$DISTILL_FS_AMD64_URL" "$DISTILL_FS_AMD64_SHA256" /distill-fs FROM ${AKERNEL_NODE_BASE_IMAGE} # Let PID 1 systemd avoid remounting shared host filesystems during shutdown. @@ -371,7 +364,8 @@ COPY --from=gvisor-runtime /gvisor/runsc /usr/local/bin/runsc COPY --from=sandboxd-builder /src/sandboxd/output/sandboxd /usr/local/bin/sandboxd COPY --from=sandboxd-builder /src/sandboxd/output/sbox /usr/local/bin/sbox COPY --from=sandboxd-builder /src/sandboxd/output/sandbox-logger /usr/local/bin/sandbox-logger -COPY --from=distill-fs-builder /src/distill-fs/target/release/distill_fs /usr/local/bin/distill_fs +COPY --from=distill-fs-runtime /distill-fs/bin/distill_fs /usr/local/bin/distill_fs +COPY --from=distill-fs-runtime /distill-fs/share/distill-fs/ /usr/local/share/distill-fs/ COPY --from=kata-runtime /kata/opt/kata /opt/kata COPY --from=runc-runtime /runc/usr/local/bin/ /usr/local/bin/ COPY --from=firecracker-runtime /firecracker/ / diff --git a/builder/scripts/install-distill-fs.sh b/builder/scripts/install-distill-fs.sh new file mode 100644 index 0000000..4581d3c --- /dev/null +++ b/builder/scripts/install-distill-fs.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Copyright (c) 2026 Ant Group Corporation. +# SPDX-License-Identifier: Apache-2.0 + +# Install the distill-fs release for AKernel. Callers pass pins from +# builder/distill-fs-versions.env, never checksums downloaded with the archive. +set -eu + +DISTILL_FS_RELEASE=$1 +DISTILL_FS_AMD64_URL=$2 +DISTILL_FS_AMD64_SHA256=$3 +destination=$4 + +case "${TARGETARCH:-$(uname -m)}" in + amd64|x86_64) ;; + *) echo "distill-fs release supports linux/amd64 only" >&2; exit 1 ;; +esac +: "${DISTILL_FS_RELEASE:?distill-fs release is not pinned}" +: "${DISTILL_FS_AMD64_URL:?distill-fs release URL is not pinned}" +: "${DISTILL_FS_AMD64_SHA256:?publish and pin the distill-fs release before building}" +printf '%s\n' "$DISTILL_FS_AMD64_SHA256" | grep -Eq '^[0-9a-f]{64}$' || { + echo "invalid distill-fs SHA-256 pin" >&2 + exit 1 +} + +# Keep the download separate from installed files. Image consumers copy only +# bin/ and share/ from this staging directory. +mkdir -p "$destination/download" "$destination/bin" "$destination/share/distill-fs" +archive="$destination/download/distill-fs.tar.gz" +curl -fSL --retry 5 --retry-delay 2 --retry-all-errors \ + "$DISTILL_FS_AMD64_URL" -o "$archive" +printf '%s %s\n' "$DISTILL_FS_AMD64_SHA256" "$archive" | sha256sum -c - +tar -xzf "$archive" -C "$destination/download" \ + distill_fs manifest.json LICENSE NOTICE Cargo.lock +bundle="$destination/download" +jq -e --arg release "$DISTILL_FS_RELEASE" \ + '.component == "distill-fs" and .release_tag == $release and + .version == ($release | ltrimstr("v")) and + .repository == "inclusionAI/distill-fs" and + .target == "x86_64-unknown-linux-musl" and + (.source_revision | test("^[0-9a-f]{40}$")) and + (.binary_sha256 | test("^[0-9a-f]{64}$"))' \ + "$bundle/manifest.json" >/dev/null +printf '%s %s\n' "$(jq -r .binary_sha256 "$bundle/manifest.json")" \ + "$bundle/distill_fs" | sha256sum -c - +readelf -h "$bundle/distill_fs" >/dev/null +if readelf -l "$bundle/distill_fs" | grep -q INTERP || + readelf -d "$bundle/distill_fs" | grep -q NEEDED; then + echo "distill-fs release must be a static executable" >&2 + exit 1 +fi +chmod 0755 "$bundle/distill_fs" +test "$("$bundle/distill_fs" --version)" = "distill_fs ${DISTILL_FS_RELEASE#v}" +install -m 0755 "$bundle/distill_fs" "$destination/bin/distill_fs" +install -m 0644 "$bundle/manifest.json" "$bundle/LICENSE" \ + "$bundle/NOTICE" "$bundle/Cargo.lock" "$destination/share/distill-fs/" diff --git a/builder/scripts/test-install-distill-fs.py b/builder/scripts/test-install-distill-fs.py new file mode 100644 index 0000000..c8c3aab --- /dev/null +++ b/builder/scripts/test-install-distill-fs.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Exercise the AKernel installer with a real candidate/release archive. + +Usage: python3 builder/scripts/test-install-distill-fs.py /path/to/release.tar.gz +Requires curl, jq, binutils, and a Linux/amd64 host. No network is needed. +""" + +import hashlib +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import tarfile +import tempfile + + +archive = Path(sys.argv[1]).resolve() +installer = Path(__file__).with_name("install-distill-fs.sh") +with tarfile.open(archive) as bundle: + files = {name: bundle.extractfile(name).read() for name in ( + "distill_fs", "manifest.json", "LICENSE", "NOTICE", "Cargo.lock" + )} +manifest = json.loads(files["manifest.json"]) +release = manifest["release_tag"] +digest = hashlib.sha256(archive.read_bytes()).hexdigest() + +with tempfile.TemporaryDirectory(prefix="distill-fs-installer-") as work: + work = Path(work) + + def check(name, asset=archive, checksum=digest, tag=release, arch="amd64", error=None): + destination = work / name + result = subprocess.run( + ["sh", str(installer), tag, asset.as_uri(), checksum, str(destination)], + env={**os.environ, "TARGETARCH": arch}, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + ) + binary = destination / "bin/distill_fs" + if error is None: + assert result.returncode == 0, result.stdout + assert binary.read_bytes() == files["distill_fs"] + for filename in ("manifest.json", "LICENSE", "NOTICE", "Cargo.lock"): + assert (destination / "share/distill-fs" / filename).read_bytes() == files[filename] + else: + assert result.returncode != 0, name + assert error in result.stdout, result.stdout + assert not binary.exists(), "a rejected artifact must not be installed" + print(f"PASS {name}") + + def altered_archive(name, binary, binary_hash): + data = {**files, "distill_fs": binary} + metadata = {**manifest, "binary_sha256": binary_hash} + data["manifest.json"] = json.dumps(metadata).encode() + path = work / f"{name}.tar.gz" + with tarfile.open(path, "w:gz") as tar: + for filename, content in data.items(): + entry = tarfile.TarInfo(filename) + entry.size = len(content) + tar.addfile(entry, io.BytesIO(content)) + return path, hashlib.sha256(path.read_bytes()).hexdigest() + + check("valid") + check("missing-pin", checksum="", error="publish and pin") + check("bad-pin", checksum="not-a-digest", error="invalid distill-fs SHA-256") + check("corrupt-archive", checksum="0" * 64, error="FAILED") + check("wrong-version", tag="v999.0.0", error="") + check("unsupported-arch", arch="arm64", error="linux/amd64 only") + bad, sha = altered_archive("bad-binary-hash", files["distill_fs"], "0" * 64) + check("bad-binary-hash", asset=bad, checksum=sha, error="FAILED") + # A correctly checksummed bundle must still reject a dynamic executable. + dynamic = Path("/bin/true").read_bytes() + headers = subprocess.check_output(["readelf", "-l", "/bin/true"], text=True) + assert "INTERP" in headers, "this negative fixture needs a dynamic /bin/true" + bad, sha = altered_archive("dynamic-binary", dynamic, hashlib.sha256(dynamic).hexdigest()) + check("dynamic-binary", asset=bad, checksum=sha, error="must be a static executable") diff --git a/deploy/README.md b/deploy/README.md index 6788897..c4457f0 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -367,3 +367,11 @@ deploy/ ├── terraform/ # multi-cloud provisioning (aliyun, huaweicloud, shared) └── scripts/ # deployment and image helper scripts ``` + +### distill-fs release dependency + +The all-in-one image downloads the static Linux/amd64 distill-fs release pinned in `builder/distill-fs-versions.env`. It does not compile `src/distill-fs`; that checkout is optional source reference. `make versions` reports the release tag and archive SHA-256. The AKernel installer at `builder/scripts/install-distill-fs.sh` checks the archive, package provenance, CLI version, and static ELF linkage, and retains licenses and provenance in `/usr/local/share/distill-fs`. + +Publish and verify the distill-fs release before updating the AKernel version, URL, and checksum pin together. Missing or invalid pins stop `make build` before either image is built. There is no source-build fallback. + +Validate installation against a downloaded candidate or release with `python3 builder/scripts/test-install-distill-fs.py /path/to/distill-fs-vX.Y.Z-linux-amd64.tar.gz` on Linux/amd64 with curl, jq, and binutils. This checks normal installation and rejects missing/invalid pins, corrupted archives, version/architecture mismatch, binary hash mismatch, and dynamically linked executables. The sandboxd pipeline and gitlink are independent of this dependency. diff --git a/deploy/scripts/build-image.sh b/deploy/scripts/build-image.sh index 53acd8d..0912c23 100755 --- a/deploy/scripts/build-image.sh +++ b/deploy/scripts/build-image.sh @@ -21,6 +21,12 @@ if [[ ! -f "${runtime_versions_file}" ]]; then fi # shellcheck source=/dev/null source "${runtime_versions_file}" +distill_fs_versions_file="${ROOT}/builder/distill-fs-versions.env" +if [[ ! -f "${distill_fs_versions_file}" ]]; then + die "missing distill-fs version manifest: ${distill_fs_versions_file}" +fi +# shellcheck source=/dev/null +source "${distill_fs_versions_file}" gvisor_release="${GVISOR_RELEASE:-}" gvisor_amd64_sha512="${GVISOR_AMD64_SHA512:-}" gvisor_amd64_url="${GVISOR_AMD64_URL:-}" @@ -59,20 +65,6 @@ component_version() { printf '%s\n' "${version}" } -package_version() { - local manifest="$1" - awk ' - /^\[package\]$/ { in_package = 1; next } - /^\[/ { in_package = 0 } - in_package && $1 == "version" { - value = $3 - gsub(/^"|"$/, "", value) - print value - exit - } - ' "${manifest}" -} - while [[ $# -gt 0 ]]; do case "$1" in --env) @@ -158,31 +150,33 @@ all_in_one_image="${repository}:${tag}" cd "${AKERNEL_REPO_ROOT}" sandboxd_source="${AKERNEL_REPO_ROOT}/src/sandboxd" -distill_fs_source="${AKERNEL_REPO_ROOT}/src/distill-fs" akernel_version="$(component_version "${AKERNEL_REPO_ROOT}")" akernel_revision="$(component_revision "${AKERNEL_REPO_ROOT}" akernel)" sandboxd_version="$(sed -n '1p' "${sandboxd_source}/version/VERSION")" sandboxd_revision="$(component_revision "${sandboxd_source}" sandboxd)" -distill_fs_version="$(package_version "${distill_fs_source}/Cargo.toml")" -distill_fs_revision="$(component_revision "${distill_fs_source}" distill-fs)" +distill_fs_version="${DISTILL_FS_RELEASE:-unpublished}" +distill_fs_revision="sha256:${DISTILL_FS_AMD64_SHA256:-pending-publication}" if [[ -z "${sandboxd_version}" ]]; then die "failed to read sandboxd version from ${sandboxd_source}/version/VERSION" fi -if [[ -z "${distill_fs_version}" ]]; then - die "failed to read distill-fs package version from ${distill_fs_source}/Cargo.toml" -fi info "component versions: akernel=${akernel_version} sandboxd=${sandboxd_version} distill-fs=${distill_fs_version}" if [[ "${print_component_versions}" == "1" ]]; then - printf '%-12s %-24s %s\n' COMPONENT VERSION REVISION + printf '%-12s %-24s %s\n' COMPONENT VERSION REVISION_OR_DIGEST printf '%-12s %-24s %s\n' akernel "${akernel_version}" "${akernel_revision}" printf '%-12s %-24s %s\n' sandboxd "${sandboxd_version}" "${sandboxd_revision}" printf '%-12s %-24s %s\n' distill-fs "${distill_fs_version}" "${distill_fs_revision}" exit 0 fi +# Fail before building either image if the release has not been published/pinned. +if [[ -z "${DISTILL_FS_RELEASE:-}" || -z "${DISTILL_FS_AMD64_URL:-}" || + ! "${DISTILL_FS_AMD64_SHA256:-}" =~ ^[0-9a-f]{64}$ ]]; then + die "publish and pin DISTILL_FS_RELEASE, DISTILL_FS_AMD64_URL, and DISTILL_FS_AMD64_SHA256 in ${distill_fs_versions_file} before building" +fi + runtime_build_args=() if [[ -n "${rrt_runtime_url}" || -n "${rrt_runtime_sha256}" ]]; then if [[ -z "${rrt_runtime_url}" || -z "${rrt_runtime_sha256}" ]]; then @@ -204,6 +198,9 @@ docker build \ info "building ${all_in_one_image}" node_build_args=( + --build-arg "DISTILL_FS_RELEASE=${DISTILL_FS_RELEASE}" + --build-arg "DISTILL_FS_AMD64_URL=${DISTILL_FS_AMD64_URL}" + --build-arg "DISTILL_FS_AMD64_SHA256=${DISTILL_FS_AMD64_SHA256}" --build-arg "AKERNEL_RUNTIME_IMAGE=${runtime_image}" --build-arg "AKERNEL_RUNTIME_PROFILE=${runtime_profile}" --build-arg "AKERNEL_ENABLE_KATA=${AKERNEL_ENABLE_KATA:-true}" diff --git a/deploy/scripts/check-prereqs.sh b/deploy/scripts/check-prereqs.sh index 9a11496..b3e0250 100755 --- a/deploy/scripts/check-prereqs.sh +++ b/deploy/scripts/check-prereqs.sh @@ -32,10 +32,18 @@ info "required tools are available" for source_file in \ "${AKERNEL_REPO_ROOT}/src/sandboxd/go.mod" \ "${AKERNEL_REPO_ROOT}/src/sandboxd/version/VERSION" \ - "${AKERNEL_REPO_ROOT}/src/distill-fs/Cargo.toml"; do + "${AKERNEL_REPO_ROOT}/src/sandboxd/third_party/runtime-versions.env"; do if [[ ! -f "${source_file}" ]]; then - die "missing submodule source ${source_file}; run git submodule update --init --recursive" + die "missing submodule source ${source_file}; run git submodule update --init src/sandboxd" fi done -info "runtime source submodules are available" +for source_file in \ + "${AKERNEL_REPO_ROOT}/builder/distill-fs-versions.env" \ + "${AKERNEL_REPO_ROOT}/builder/scripts/install-distill-fs.sh"; do + if [[ ! -f "${source_file}" ]]; then + die "missing distill-fs release build input ${source_file}; restore it from the AKernel checkout" + fi +done + +info "runtime sources and release build inputs are available"