From 17a196b7dcf7d949bf42138f33d6a1adcdb753b9 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sat, 10 Oct 2026 17:45:10 +0000 Subject: [PATCH] Upgrade microsandbox to the official 0.7.8 release --- Makefile | 10 +-- docs/maintainers.md | 8 +- docs/zh/maintainers.md | 10 +-- scripts/build-core-distribution.sh | 10 +-- scripts/build-microsandbox-provider.py | 90 +++++++++++++++++++ scripts/build-microsandbox-provider.test.py | 48 ++++++++++ scripts/ci_plan.py | 1 + scripts/ci_plan_test.py | 4 + scripts/core-distribution-manifest.py | 6 +- .../internal/sandbox/microsandbox/types.go | 2 +- .../tools/microsandbox-provider/README.md | 12 +-- .../tools/microsandbox-provider/bootstrap.go | 2 +- .../tools/microsandbox-provider/deployment.go | 2 +- .../core/tools/microsandbox-provider/go.mod | 2 +- .../core/tools/microsandbox-provider/go.sum | 4 +- .../core/tools/microsandbox-provider/main.go | 23 ++++- .../microsandbox-provider/sdk_release_test.go | 28 ++++++ 17 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 scripts/build-microsandbox-provider.py create mode 100644 scripts/build-microsandbox-provider.test.py create mode 100644 services/core/tools/microsandbox-provider/sdk_release_test.go diff --git a/Makefile b/Makefile index 888e671c2..9de40a1a8 100644 --- a/Makefile +++ b/Makefile @@ -166,17 +166,13 @@ build-mcode-runtime: .PHONY: build-microsandbox-provider check-microsandbox-provider build-microsandbox-provider: - @test "$$(go env GOOS)" = linux || { echo 'The microsandbox provider helper requires Linux' >&2; exit 1; } - @set -e; output="$${OAC_DEV_HOME:-$$HOME/.oac}/build/microsandbox-provider"; \ - [[ "$$output" == /* ]] || { echo 'Provider output directory must be absolute' >&2; exit 1; }; \ - mkdir -p "$$output"; \ - cd services/core/tools/microsandbox-provider; \ - GOWORK=off CGO_ENABLED=1 go build -mod=readonly -trimpath -o "$$output/oac-microsandbox-provider" . + python3 scripts/build-microsandbox-provider.py build "$${OAC_DEV_HOME:-$$HOME/.oac}/build/microsandbox-provider/oac-microsandbox-provider" check-microsandbox-provider: + python3 scripts/build-microsandbox-provider.test.py go test -mod=readonly ./services/core/internal/sandbox/microsandbox/... -count=1 @if [[ "$$(go env GOOS)" == linux ]]; then \ - cd services/core/tools/microsandbox-provider && GOWORK=off CGO_ENABLED=1 go test -mod=readonly ./... -count=1; \ + python3 scripts/build-microsandbox-provider.py test; \ else \ printf 'Skipping the Linux-only microsandbox SDK helper tests; the full Linux gate is required before release.\n'; \ fi diff --git a/docs/maintainers.md b/docs/maintainers.md index d0154a883..c3c8f33fc 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -33,7 +33,7 @@ make build-core-distribution | `OAC_NATIVE_INSTALLER_BUILD_DIR` | Native installer catalog directory; see [Native installers](#native-installers) | | `CORE_DISTRIBUTION_BUILD_DIR` | Output directory under `~/.oac`. Default: `~/.oac/build/core-distribution` | | `CORE_DISTRIBUTION_BUILD_NETWORK` | Docker build network: `default`, `host` or `none` | -| `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | Cached microsandbox release archive. Default: `~/.oac/cache/microsandbox-v0.7.2-linux-x86_64.tar.gz`, downloaded when missing | +| `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | Cached microsandbox release archive. Default: `~/.oac/cache/microsandbox-v0.7.8-linux-x86_64.tar.gz`, downloaded when missing | | `CORE_DISTRIBUTION_DATABASE_IMAGE` | PostgreSQL 16 image; the default is pinned by its linux/amd64 manifest digest | The build reuses the Core, Web, Runtime, SDK and helper builders. The manifest records the commit and source tree, image config and OCI manifest digests, the Runtime OCI manifest digest, the microsandbox runtime and firmware hashes, and the size and SHA-256 of every Runtime and node artifact; native installers carry only their SHA-256 in the [catalog](#native-installers). Output is the control archive and its `.sha256`, the optional offline archive, and the versioned Runtime, node and native installer assets. Nothing is published. Rebuilding into a directory that already holds this commit's distribution is refused. @@ -103,16 +103,16 @@ make build-e2b-provider Docker builds the Linux helper for `GOARCH=amd64` (default) or `GOARCH=arm64` with the pinned CPython and Debian 12 image. The Python dependency closure, including PyInstaller, is hash-locked in `services/core/tools/e2b-provider/requirements.lock`; no E2B account key is needed. Set `E2B_PROVIDER_BUILD_DIR` for another output directory. The build is a pure function of the helper sources, `LICENSE` and the build script, so it is cached under `~/.oac/cache/e2b-provider/` by their hash and rebuilt only when they change. The output is `oac-e2b-provider-linux-.tar.gz` with its `.sha256`; it extracts to `oac-e2b-provider/` with the executable, `_internal/`, `licenses/`, `requirements.lock` and `manifest.json`. The Core image uses that tree; the host needs a compatible glibc and CA certificates, not Python. -**microsandbox helper.** Linux only, with a C compiler: +**microsandbox helper.** Linux amd64 only, with a C compiler: ```sh make build-microsandbox-provider make check-microsandbox-provider ``` -The helper is written to `~/.oac/build/microsandbox-provider/oac-microsandbox-provider`. Its separate Go module pins the microsandbox Go SDK v0.7.2 and embeds the matching FFI library; never build production with the SDK's `microsandbox_ffi_path` tag. Core itself stays a CGO-disabled build. The helper needs glibc and runs only on nodes. +The helper is written to `~/.oac/build/microsandbox-provider/oac-microsandbox-provider`. Its separate Go module pins the official microsandbox SDK source commit. Both commands and the distribution build use `scripts/build-microsandbox-provider.py`: it stages the module dependencies, verifies the matching official FFI release checksum, fills the SDK's empty release bundle and builds with that FFI embedded. The SDK source is unchanged and no vendored binary is committed. Use this entry point rather than invoking `go build` directly; never build production with the SDK's `microsandbox_ffi_path` tag. Core itself stays a CGO-disabled build. The helper needs glibc and runs only on nodes. -**microsandbox runtime.** The distribution uses the official [v0.7.2 release](https://github.com/superradcompany/microsandbox/releases/tag/v0.7.2) archive `microsandbox-linux-x86_64.tar.gz`, SHA256 `47c223e3ef5298abf05f47ed9f87981106e400d99bb3f1d042d4d6881346b18b` (`RUNTIME_ARCHIVE_SHA256` in `scripts/core-distribution-manifest.py`). The build verifies the checksum before extracting `msb` and `libkrunfw.so.5.6.1` and records both files' hashes. The helper checks those hashes on every call and never installs or upgrades them. +**microsandbox runtime.** The distribution uses the official [v0.7.8 release](https://github.com/superradcompany/microsandbox/releases/tag/v0.7.8) archive `microsandbox-linux-x86_64.tar.gz`, SHA256 `86f9f72dc3e639c7175bc07909b4b63ce412517c1ef8a2e1921171af5682fded` (`RUNTIME_ARCHIVE_SHA256` in `scripts/core-distribution-manifest.py`). The build verifies the checksum before extracting `msb` and `libkrunfw.so.5.6.1` and records both files' hashes. The helper checks those hashes on every call and never installs or upgrades them. ### Standalone Core builds diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index e61ec78da..de88bb348 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 494618f4d605d96bfcb9b1413f8224722324e1037764cc5b38baca102a8f1e90 +source_hash: f91505a6502e09cc13b5ece7cb4ba578f6d2861621d70a1f836f89dcf9299ea4 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -35,7 +35,7 @@ make build-core-distribution | `OAC_NATIVE_INSTALLER_BUILD_DIR` | 原生安装器目录;请参阅[原生安装器](#native-installers) | | `CORE_DISTRIBUTION_BUILD_DIR` | `~/.oac` 下的输出目录。默认值:`~/.oac/build/core-distribution` | | `CORE_DISTRIBUTION_BUILD_NETWORK` | Docker 构建网络:`default`、`host` 或 `none` | -| `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | 已缓存的 microsandbox 发布归档。默认值:`~/.oac/cache/microsandbox-v0.7.2-linux-x86_64.tar.gz`,缺失时下载 | +| `CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE` | 已缓存的 microsandbox 发布归档。默认值:`~/.oac/cache/microsandbox-v0.7.8-linux-x86_64.tar.gz`,缺失时下载 | | `CORE_DISTRIBUTION_DATABASE_IMAGE` | PostgreSQL 16 镜像;默认值通过其 linux/amd64 清单摘要固定 | 构建过程会复用 Core、Web、Runtime、SDK 和辅助程序构建器。清单会记录提交和源代码树、镜像配置及 OCI 清单摘要、Runtime OCI 清单摘要、microsandbox 运行时和固件哈希,以及每个 Runtime 和节点构件的大小与 SHA-256;原生安装器在[目录](#native-installers)中仅记录其 SHA-256。输出包括控制归档及其 `.sha256`、可选的离线归档,以及带版本号的 Runtime、节点和原生安装器资源。此过程不会发布任何内容。如果目标目录中已包含此提交的分发包,重建会拒绝执行。 @@ -105,16 +105,16 @@ make build-e2b-provider Docker 使用固定版本的 CPython 和 Debian 12 镜像按 `GOARCH=amd64`(默认)或 `GOARCH=arm64` 构建 Linux 辅助程序。Python 依赖闭包(including PyInstaller)在 `services/core/tools/e2b-provider/requirements.lock` 中按哈希锁定;不需要 E2B 账户密钥。要使用其他输出目录,请设置 `E2B_PROVIDER_BUILD_DIR`。构建结果完全由辅助程序源代码、`LICENSE` 和构建脚本决定,因此会按它们的哈希缓存在 `~/.oac/cache/e2b-provider/` 下,仅在它们变化时重新构建。输出为 `oac-e2b-provider-linux-.tar.gz` 及其 `.sha256`;解压后会得到 `oac-e2b-provider/`,其中包含可执行文件、`_internal/`、`licenses/`、`requirements.lock` 和 `manifest.json`。Core 镜像使用该目录树;主机需要兼容的 glibc 和 CA 证书,而不需要 Python。 -**microsandbox 辅助程序。** 仅支持 Linux,并且需要 C 编译器: +**microsandbox 辅助程序。** 仅支持 Linux amd64,并且需要 C 编译器: ```sh make build-microsandbox-provider make check-microsandbox-provider ``` -该辅助程序会写入 `~/.oac/build/microsandbox-provider/oac-microsandbox-provider`。其独立的 Go 模块固定 microsandbox Go SDK v0.7.2,并嵌入匹配的 FFI 库;构建生产版本时,绝不能使用该 SDK 的 `microsandbox_ffi_path` 标签。Core 本身仍采用禁用 CGO 的构建。该辅助程序需要 glibc,并且只能在节点上运行。 +该辅助程序会写入 `~/.oac/build/microsandbox-provider/oac-microsandbox-provider`。其独立的 Go 模块固定官方 microsandbox SDK 源码提交。上述两个命令与分发构建均使用 `scripts/build-microsandbox-provider.py`:它暂存模块依赖,验证匹配的官方 FFI 发行文件校验和,填充 SDK 的空发行包,并构建嵌入该 FFI 的程序。SDK 源码保持不变,仓库不提交 vendored 二进制文件。请使用此入口,而非直接调用 `go build`;构建生产版本时,绝不能使用该 SDK 的 `microsandbox_ffi_path` 标签。Core 本身仍采用禁用 CGO 的构建。该辅助程序需要 glibc,并且只能在节点上运行。 -**microsandbox 运行时。** 分发包使用官方的 [v0.7.2 release](https://github.com/superradcompany/microsandbox/releases/tag/v0.7.2) 归档 `microsandbox-linux-x86_64.tar.gz`,SHA256 为 `47c223e3ef5298abf05f47ed9f87981106e400d99bb3f1d042d4d6881346b18b`(即 `scripts/core-distribution-manifest.py` 中的 `RUNTIME_ARCHIVE_SHA256`)。构建过程会先验证校验和,再解压 `msb` 和 `libkrunfw.so.5.6.1`,并记录这两个文件的哈希。辅助程序会在每次调用时检查这些哈希,并且绝不安装或升级它们。 +**microsandbox 运行时。** 分发包使用官方的 [v0.7.8 release](https://github.com/superradcompany/microsandbox/releases/tag/v0.7.8) 归档 `microsandbox-linux-x86_64.tar.gz`,SHA256 为 `86f9f72dc3e639c7175bc07909b4b63ce412517c1ef8a2e1921171af5682fded`(即 `scripts/core-distribution-manifest.py` 中的 `RUNTIME_ARCHIVE_SHA256`)。构建过程会先验证校验和,再解压 `msb` 和 `libkrunfw.so.5.6.1`,并记录这两个文件的哈希。辅助程序会在每次调用时检查这些哈希,并且绝不安装或升级它们。 ### 独立 Core 构建 {#standalone-core-builds} diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index ac40a609a..37da251f5 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -110,16 +110,12 @@ cp services/core/deploy/codex/seccomp.json "$bundle/runtime/" cp LICENSE "$bundle/" OAC_DEV_BUILD_REVISION="$revision" scripts/build-core-image-context.sh "$stage/core" -( - cd services/core/tools/microsandbox-provider - GOWORK=off CGO_ENABLED=1 go build -mod=readonly -trimpath \ - -o "$stage/core/bin/oac-microsandbox-provider" . -) -msb_archive="${CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE:-$runtime_root/cache/microsandbox-v0.7.2-linux-x86_64.tar.gz}" +python3 scripts/build-microsandbox-provider.py build "$stage/core/bin/oac-microsandbox-provider" +msb_archive="${CORE_DISTRIBUTION_MICROSANDBOX_ARCHIVE:-$runtime_root/cache/microsandbox-v0.7.8-linux-x86_64.tar.gz}" if [[ ! -f "$msb_archive" ]]; then mkdir -p "$(dirname "$msb_archive")" curl --fail --location --proto '=https' --tlsv1.2 \ - https://github.com/superradcompany/microsandbox/releases/download/v0.7.2/microsandbox-linux-x86_64.tar.gz \ + https://github.com/superradcompany/microsandbox/releases/download/v0.7.8/microsandbox-linux-x86_64.tar.gz \ --output "$stage/microsandbox.download" python3 scripts/core-distribution-manifest.py extract-runtime "$stage/microsandbox.download" "$stage/core/microsandbox" mv "$stage/microsandbox.download" "$msb_archive" diff --git a/scripts/build-microsandbox-provider.py b/scripts/build-microsandbox-provider.py new file mode 100644 index 000000000..2e5e31f90 --- /dev/null +++ b/scripts/build-microsandbox-provider.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Build the provider with the official SDK source and matching embedded FFI.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +SDK_MODULE = "github.com/superradcompany/microsandbox/sdk/go" +FFI_SHA256 = "bc079888050a92d3652191ae8e18fba96e1ac66dc78bce4e5aaeb7eca9b04e25" +FFI_FILE = "libmicrosandbox_go_ffi-linux-amd64.so" + + +def verify_ffi(path): + with Path(path).open("rb") as stream: + digest = hashlib.sha256() + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + if digest.hexdigest() != FFI_SHA256: + raise ValueError("Official microsandbox FFI checksum mismatch") + + +def install_ffi(source, destination): + verify_ffi(source) + if destination.is_symlink() or not destination.is_file() or destination.stat().st_size != 0: + raise ValueError("Official SDK must contain an empty FFI release sentinel") + shutil.copyfile(source, destination) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("operation", choices=("build", "test")) + parser.add_argument("output", nargs="?", type=Path) + args = parser.parse_args() + if (args.operation == "build") != (args.output is not None): + parser.error("build requires an absolute output path; test takes no output") + if args.output is not None and not args.output.is_absolute(): + parser.error("output must be absolute") + root = Path(__file__).resolve().parents[1] + module = root / "services/core/tools/microsandbox-provider" + env = dict(os.environ, GOWORK="off", CGO_ENABLED="1") + def run(*command, cwd=module): + try: + return subprocess.check_output(command, cwd=cwd, env=env, text=True) + except subprocess.CalledProcessError as error: + print(error.output, end="") + raise + if run("go", "env", "GOOS", "GOARCH").splitlines() != ["linux", "amd64"]: + parser.error("the provider distribution requires Linux amd64") + run("go", "mod", "download", SDK_MODULE) + sdk = json.loads(run("go", "list", "-mod=readonly", "-m", "-json", SDK_MODULE)) + if "Replace" in sdk: + raise ValueError("The SDK must be the pinned official module") + versions = re.findall(r'^const sdkVersion = "([0-9.]+)"$', (Path(sdk["Dir"]) / "setup.go").read_text(), re.M) + if len(versions) != 1: + raise ValueError("Official SDK release declaration changed") + version = versions[0] + cache = Path(os.environ.get("OAC_DEV_HOME", str(Path.home() / ".oac"))) / "cache" + if not cache.is_absolute(): + raise ValueError("OAC_DEV_HOME must be absolute") + cache.mkdir(parents=True, exist_ok=True) + ffi = cache / (FFI_SHA256 + ".so") + with tempfile.TemporaryDirectory(prefix="oac-microsandbox-build-", dir=cache) as temporary: + stage = Path(temporary) + if not ffi.exists(): + download = stage / FFI_FILE + run("curl", "--fail", "--location", "--silent", "--show-error", "--retry", "3", "--output", str(download), + f"https://github.com/superradcompany/microsandbox/releases/download/v{version}/{FFI_FILE}") + verify_ffi(download) + os.replace(download, ffi) + verify_ffi(ffi) + for source in [*module.glob("*.go"), module / "go.mod", module / "go.sum"]: + shutil.copyfile(source, stage / source.name) + run("go", "mod", "vendor", "-o", str(stage / "vendor")) + install_ffi(ffi, stage / "vendor" / SDK_MODULE / "internal/bundle/bundles" / FFI_FILE) + if args.operation == "test": + print(run("go", "test", "-mod=vendor", "./...", "-count=1", cwd=stage), end="") + print(run("go", "vet", "-mod=vendor", "./...", cwd=stage), end="") + else: + args.output.parent.mkdir(parents=True, exist_ok=True) + print(run("go", "build", "-mod=vendor", "-trimpath", "-o", str(args.output), ".", cwd=stage), end="") + + +if __name__ == "__main__": + main() diff --git a/scripts/build-microsandbox-provider.test.py b/scripts/build-microsandbox-provider.test.py new file mode 100644 index 000000000..d53f80ccd --- /dev/null +++ b/scripts/build-microsandbox-provider.test.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import hashlib +import importlib.util +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location("build_provider", Path(__file__).with_name("build-microsandbox-provider.py")) +build = importlib.util.module_from_spec(spec) +spec.loader.exec_module(build) + + +class OfficialBundleTests(unittest.TestCase): + def test_only_verified_payload_replaces_empty_sentinel(self): + with tempfile.TemporaryDirectory() as directory: + source, destination = Path(directory) / "ffi", Path(directory) / "sentinel" + source.write_bytes(b"official ffi") + destination.touch() + with patch.object(build, "FFI_SHA256", hashlib.sha256(source.read_bytes()).hexdigest()): + build.install_ffi(source, destination) + self.assertEqual(destination.read_bytes(), source.read_bytes()) + with self.assertRaisesRegex(ValueError, "empty FFI"): + build.install_ffi(source, destination) + + def test_corrupt_payload_does_not_replace_sentinel(self): + with tempfile.TemporaryDirectory() as directory: + source, destination = Path(directory) / "ffi", Path(directory) / "sentinel" + source.write_bytes(b"wrong release") + destination.touch() + with self.assertRaisesRegex(ValueError, "checksum mismatch"): + build.install_ffi(source, destination) + self.assertEqual(destination.read_bytes(), b"") + + def test_symlink_sentinel_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + source, destination, outside = (Path(directory) / name for name in ("ffi", "sentinel", "outside")) + source.write_bytes(b"official ffi") + outside.touch() + destination.symlink_to(outside) + with patch.object(build, "FFI_SHA256", hashlib.sha256(source.read_bytes()).hexdigest()): + with self.assertRaisesRegex(ValueError, "empty FFI"): + build.install_ffi(source, destination) + self.assertEqual(outside.read_bytes(), b"") + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 02f0f47f6..19a0fadd2 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -78,6 +78,7 @@ (("scripts/build-native-", "scripts/native-"), SCRIPTS, ("native", "backend", "distribution")), (("scripts/build-core.sh", "scripts/build-core-image-context.sh"), (".sh",), ("backend", "api", "distribution", "native")), (("deploy/distribution/",), ("Dockerfile",), ("backend", "api", "distribution", "native", "compose")), + (("scripts/build-microsandbox-provider.",), (".py",), ("backend", "distribution")), (("scripts/build-e2b-provider.sh",), (".sh",), ("backend", "api", "distribution")), (("scripts/build-claude", "scripts/check-claude", "scripts/build-mcode", "scripts/prepare-release-runtimes.sh"), SCRIPTS, ("harness", "native", "backend", "distribution")), diff --git a/scripts/ci_plan_test.py b/scripts/ci_plan_test.py index d5ee84204..40ec23043 100644 --- a/scripts/ci_plan_test.py +++ b/scripts/ci_plan_test.py @@ -57,6 +57,10 @@ def test_generated_outputs_keep_freshness_checks(self): with self.subTest(path=path): self.assertIn("distribution", self.jobs(path)) + def test_microsandbox_builder_keeps_helper_and_distribution_checks(self): + for path in ("scripts/build-microsandbox-provider.py", "scripts/build-microsandbox-provider.test.py"): + self.assertTrue({"backend", "distribution"} <= self.jobs(path)) + def test_distribution_image_build_keeps_api_acceptance(self): plan = ci.select(["scripts/build-core-distribution.sh"]) self.assertTrue(plan["image"]) diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index 3529fb8fe..cdb7e9843 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -17,7 +17,7 @@ import zipapp -RUNTIME_ARCHIVE_SHA256 = "47c223e3ef5298abf05f47ed9f87981106e400d99bb3f1d042d4d6881346b18b" +RUNTIME_ARCHIVE_SHA256 = "86f9f72dc3e639c7175bc07909b4b63ce412517c1ef8a2e1921171af5682fded" DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "deploy/node")) import provider_assets @@ -204,7 +204,7 @@ def verify_runtime(image, daemon, source): def extract_runtime(archive, destination): if sha256(archive) != RUNTIME_ARCHIVE_SHA256: - raise ValueError("microsandbox v0.7.2 release checksum mismatch") + raise ValueError("microsandbox v0.7.8 release checksum mismatch") destination = pathlib.Path(destination) destination.mkdir(parents=True, exist_ok=True) with tarfile.open(archive, "r:gz") as bundle: @@ -346,7 +346,7 @@ def node_payload(bundle, stage, revision, source_tree, artifact_base_url="", off "image_manifest_digests": {name: identity[1] for name, identity in identities.items()}, "runtime_ref": "oac-runtime@" + digest, "microsandbox": { - "version": "0.7.2", + "version": "0.7.8", "runtime_sha256": sha256(stage / "core/microsandbox/msb"), "firmware_sha256": sha256(stage / "core/microsandbox/libkrunfw.so.5.6.1"), }, diff --git a/services/core/internal/sandbox/microsandbox/types.go b/services/core/internal/sandbox/microsandbox/types.go index bb8d9246d..68eb8b3d3 100644 --- a/services/core/internal/sandbox/microsandbox/types.go +++ b/services/core/internal/sandbox/microsandbox/types.go @@ -10,7 +10,7 @@ import ( ) const ProtocolVersion = 4 -const SDKVersion = "v0.7.2" +const SDKVersion = "v0.7.8" const MaxOutputBytes = 1024 * 1024 const MaxRequestBytes = 72 * 1024 * 1024 const MaxResponseBytes = 16 * 1024 * 1024 diff --git a/services/core/tools/microsandbox-provider/README.md b/services/core/tools/microsandbox-provider/README.md index ac7e22c2c..243c6d682 100644 --- a/services/core/tools/microsandbox-provider/README.md +++ b/services/core/tools/microsandbox-provider/README.md @@ -1,6 +1,6 @@ # microsandbox Sandbox Provider helper -microsandbox runs each hosted Session in its own microVM on a Linux amd64 node with KVM, and it is the Sandbox Provider that supports idle suspension. Core forwards provider operations to the node over the [node protocol](../../../../contracts/agents-api/node-generation-protocol.md); the node's adapter ([`sandbox/microsandbox`](../../internal/sandbox/microsandbox)) runs this helper once per operation. The private helper wire version is 4; mismatches are rejected. The helper links the microsandbox Go SDK v0.7.2 with its FFI library, so Core and the node program stay CGO-free Go binaries. It implements the checkpoint operations of the provider-neutral `sandbox.SandboxProvider` with full snapshots. It has no daemon, lifecycle database, scheduler or network control plane. +microsandbox runs each hosted Session in its own microVM on a Linux amd64 node with KVM, and it is the Sandbox Provider that supports idle suspension. Core forwards provider operations to the node over the [node protocol](../../../../contracts/agents-api/node-generation-protocol.md); the node's adapter ([`sandbox/microsandbox`](../../internal/sandbox/microsandbox)) runs this helper once per operation. The private helper wire version is 4; mismatches are rejected. The helper links the microsandbox Go SDK v0.7.8 with its FFI library, so Core and the node program stay CGO-free Go binaries. It implements the checkpoint operations of the provider-neutral `sandbox.SandboxProvider` with full snapshots. It has no daemon, lifecycle database, scheduler or network control plane. [Add a Sandbox Provider](../../../../docs/sandbox-provider.md) owns the provider contract. [Sandbox deployment](../../../../contracts/agents-api/sandbox-deployment.md) owns the resources, Runtime release and suspension policy; the [nodes guide](../../../../docs/getting-started/nodes.md) owns node installation, host requirements, the node's directories and its network policy. @@ -8,7 +8,7 @@ microsandbox runs each hosted Session in its own microVM on a Linux amd64 node w The [maintainer guide](../../../../docs/maintainers.md#runtime-images-and-helpers) builds the helper and packages the checksum-verified `msb` runtime and `libkrunfw` firmware. The node's provider configuration, written by the node installer, supplies the absolute helper, runtime and firmware paths with their SHA-256 values, the runtime home, the Runtime image reference, the saved resources and the host network policy. -Before every operation the helper checks that it was built with the published SDK module v0.7.2 without a replacement, that the runtime and firmware match their hashes, that the runtime's `.msbver` ELF section reports the SDK version and that the SDK resolves exactly those paths with the local backend ([`main.go`](main.go)). It never installs or upgrades these files; keep them unchanged for the lifetime of the provider's backend. Ambient SDK profiles are ignored. +Before every operation the helper checks that it was built with the exact official SDK module declared in its embedded `go.mod` without a replacement, that the runtime and firmware match their hashes, that the runtime's `.msbver` ELF section reports the declared native release, that the SDK source and embedded FFI report that release, and that the SDK resolves exactly those paths with the local backend ([`main.go`](main.go)). It never installs or upgrades these files; keep them unchanged for the lifetime of the provider's backend. Ambient SDK profiles are ignored. The runtime home must be private (mode 0700), short, on local persistent storage and used by no other installation, profile or manual lifecycle tool. microsandbox uses Unix sockets there, so the node installer refuses a home whose path would exceed their limit. The home holds confidential VM disks, memory snapshots and SDK state; preserve it with the node identity and Core's database when recovering a host. Every managed lifecycle change goes through the provider. @@ -16,11 +16,11 @@ The Runtime image is an immutable `repository@sha256:<64 lowercase hex>` referen ## Create and bootstrap -Create names the VM from a hash of the installation and allocation reference plus the compute generation; a name never serves another incarnation. It creates the VM with the saved CPUs and memory as both initial and maximum, a managed root disk of `root_disk_mib`, an owned ext4 disk of `environment_disk_mib` or the explicitly resolved external directory mounted at `/environment`, user 1000:1000, working directory `/` and the node's network policy ([`bootstrap.go`](bootstrap.go)). The resource checks run before bootstrap. +Create names the VM from a hash of the installation and allocation reference plus the compute generation; a name never serves another incarnation. It creates the VM with the saved CPUs and memory as both initial and maximum, a managed root disk of `root_disk_mib`, an owned ext4 disk of `environment_disk_mib` or the explicitly resolved external directory mounted at `/environment`, user 1000:1000, working directory `/` and the node's network policy ([`bootstrap.go`](bootstrap.go)). The resource checks run before bootstrap. Creation and restore retain the SDK's strict hostname policy enforcement; the adapter does not disable it. -Workspace, staging and outputs share the `/environment` filesystem, which keeps the Runtime's cross-device and link checks intact; the layered root filesystem can report different device IDs for a directory and its upper-layer files, so it holds no workspace data. In owned mode, full snapshots and sandbox removal capture, restore and reclaim this disk. In external mode, the filesystem provider owns the directory; sandbox removal never deletes it. The binding and resource rules belong to [Independent workspace attachment](../../../../docs/sandbox-provider.md#independent-workspace-attachment). Private HOME and history remain in the checkpointed root. +Workspace, staging and outputs share the `/environment` filesystem, which keeps the Runtime's cross-device and link checks intact; the layered root filesystem can report different device IDs for a directory and its upper-layer files, so it holds no workspace data. In owned mode, full snapshots and sandbox removal capture, restore and reclaim this disk. In external mode, the filesystem provider owns the directory; sandbox removal never deletes it. The binding and resource rules belong to [Independent workspace attachment](../../../../docs/sandbox-provider.md#independent-workspace-attachment). The [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) owns the Runtime directory layout. -VM creation does not run the image's entry point. The bootstrap runs as root through confidential standard input, with a two-minute limit. It creates the Runtime directories and the private control directory `/run/oac` (mode 0700, owned by UID 1000), writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json`, bind-mounts `/environment/workspace` at `/workspace` and starts `oac-daemon connect --bootstrap-file` in the background as UID/GID 1000. `OAC_RUNTIME_DAEMON_SUSPEND_PID_FILE=/run/oac/daemon-suspend.json` enables the daemon's park and wake control. The `io.oac.bootstrap` label then changes from `pending` to `complete` through the SDK's next-start modification policy, because v0.7.2 cannot update the labels of a running VM. That label confirms only these writes and the launch, not authentication or native readiness. +VM creation does not run the image's entry point. The bootstrap runs as root through confidential standard input, with a two-minute limit. It creates the Runtime directories and the private control directory `/run/oac` (mode 0700, owned by UID 1000), writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json`, bind-mounts `/environment/workspace` at `/workspace` and starts `oac-daemon connect --bootstrap-file` in the background as UID/GID 1000. `OAC_RUNTIME_DAEMON_SUSPEND_PID_FILE=/run/oac/daemon-suspend.json` enables the daemon's park and wake control. The `io.oac.bootstrap` label then changes from `pending` to `complete` through the SDK's next-start modification policy, because v0.7.8 cannot update the labels of a running VM. That label confirms only these writes and the launch, not authentication or native readiness. A helper response carries `CreateSettled` with a configuration rejection only after native Create has completed, the first inspection has verified the exact compute ID and ownership, and the resource check has rejected the VM before bootstrap started. The adapter keeps the original error and validates the compute identity before passing the proof to Core. The private `initial_info` operation used by `GetInfo` and `Renew` can also return an exact initial `State` with `Status="absent"`, no native ID and `CreateSettled=true`, after typed native absence and durable closure of initial Create admission under the allocation lock. It accepts only generation zero without restored ancestry or a native ID. The adapter validates the complete receipt and returns absence without an error. Ordinary `inspect` used by `GetCompute`, uncertain Create outcomes, timeouts and ownership failures never produce this receipt; missing compute alone is not settlement. @@ -57,7 +57,7 @@ The command checks the protected Environment and suspension identities and the p ## Metrics -The read-only metrics operation holds the allocation lock and verifies the exact compute ID through the SDK before and after it runs `msb metrics NAME --format json` with the same pinned runtime binary ([`metrics.go`](metrics.go)). The CLI report keeps the native sample timestamp and fractional-second uptime, so the helper reconstructs one run start consistently across polls and Core restarts, and a new run gets a new start. The Go SDK's projection drops the timestamp and truncates uptime to whole seconds, so it cannot provide this; sandbox creation time is not a run start time. In the pinned source (`v0.7.2`, commit `1c59b8dbf0ad47dda2f807c0214b529aceb81c74`), `crates/metrics/lib/registry.rs` reads `sampled_at_unix_ms` and `started_at_unix_ms` together and subtracts them for uptime, and `crates/cli/lib/commands/metrics.rs` serializes the timestamp and `uptime.as_secs_f64()`. +The read-only metrics operation holds the allocation lock and verifies the exact compute ID through the SDK before and after it runs `msb metrics NAME --format json` with the same pinned runtime binary ([`metrics.go`](metrics.go)). The CLI report keeps the native sample timestamp and fractional-second uptime, so the helper reconstructs one run start consistently across polls and Core restarts, and a new run gets a new start. The Go SDK's projection drops the timestamp and truncates uptime to whole seconds, so it cannot provide this; sandbox creation time is not a run start time. In the pinned source (`v0.7.8`, commit `7b7b9dc89e9a1c77801918f7833c3381d1754579`), `crates/metrics/lib/registry.rs` reads `sampled_at_unix_ms` and `started_at_unix_ms` together and subtracts them for uptime, and `crates/cli/lib/commands/metrics.rs` serializes the timestamp and `uptime.as_secs_f64()`. The helper returns only the native observation time, exact uptime, cumulative vCPU time and guest memory usage and limit. It rejects stale or exited reports and missing or malformed fields, bounds the CLI output and reports failures only as an unavailable code, never native diagnostics. Metrics never connect to the guest, renew activity, resume paused compute or change lifecycle state. [Runtime observability](../../../../contracts/agents-api/runtime-observability.md) owns the mapping to observations. diff --git a/services/core/tools/microsandbox-provider/bootstrap.go b/services/core/tools/microsandbox-provider/bootstrap.go index d1753e4f4..79125a12d 100644 --- a/services/core/tools/microsandbox-provider/bootstrap.go +++ b/services/core/tools/microsandbox-provider/bootstrap.go @@ -107,7 +107,7 @@ func (b backend) create(ctx context.Context) (wire.Response, error) { return wire.Response{}, sandbox.ErrCommandUnconfirmed } // Persist the final bootstrap receipt without restarting the live guest. - // v0.7.2 cannot update active labels; ownership reads persisted config. + // v0.7.8 cannot update active labels; ownership reads persisted config. _, e = live.Modify(ctx, sdk.ModifyOptions{Labels: map[string]string{bootstrapLabel: "complete"}, Policy: sdk.ModificationPolicyNextStart}) if e != nil { return wire.Response{}, e diff --git a/services/core/tools/microsandbox-provider/deployment.go b/services/core/tools/microsandbox-provider/deployment.go index c950588aa..c768e0e59 100644 --- a/services/core/tools/microsandbox-provider/deployment.go +++ b/services/core/tools/microsandbox-provider/deployment.go @@ -31,7 +31,7 @@ func resourceProof(config wire.Config) string { return hex.EncodeToString(digest[:]) } -// The SDK decodes native CPU/memory/rootfs configuration. Its v0.7.2 projection +// The SDK decodes native CPU/memory/rootfs configuration. Its v0.7.8 projection // omits mounts, so the explicitly owned or external inventory is projected here. // Restored roots have no configured size: a verified full snapshot supplies that // proof, retained as a label only after inspecting the restored target. diff --git a/services/core/tools/microsandbox-provider/go.mod b/services/core/tools/microsandbox-provider/go.mod index 25d0423c5..c04558e14 100644 --- a/services/core/tools/microsandbox-provider/go.mod +++ b/services/core/tools/microsandbox-provider/go.mod @@ -4,7 +4,7 @@ go 1.26.8 require ( github.com/MiniMax-AI/OpenAgentCore v0.0.0 - github.com/superradcompany/microsandbox/sdk/go v0.7.2 + github.com/superradcompany/microsandbox/sdk/go v0.0.0-20261009155513-7b7b9dc89e9a ) require ( diff --git a/services/core/tools/microsandbox-provider/go.sum b/services/core/tools/microsandbox-provider/go.sum index 77cb4e04c..a92270ad0 100644 --- a/services/core/tools/microsandbox-provider/go.sum +++ b/services/core/tools/microsandbox-provider/go.sum @@ -1,6 +1,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/superradcompany/microsandbox/sdk/go v0.7.2 h1:aO70srBRgu50rNZTtzQTOO3xxPSthkZLkq3kjWvmsqg= -github.com/superradcompany/microsandbox/sdk/go v0.7.2/go.mod h1:p7Tm/p9zkO7sHO3N8A1fiTVeLB2w/fQoKdYlpN/5neA= +github.com/superradcompany/microsandbox/sdk/go v0.0.0-20261009155513-7b7b9dc89e9a h1:r9mZpz3z3E5tXRWDulZSs0gyM2EEhsTUqUjyZQ6MT4w= +github.com/superradcompany/microsandbox/sdk/go v0.0.0-20261009155513-7b7b9dc89e9a/go.mod h1:p7Tm/p9zkO7sHO3N8A1fiTVeLB2w/fQoKdYlpN/5neA= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= diff --git a/services/core/tools/microsandbox-provider/main.go b/services/core/tools/microsandbox-provider/main.go index c16a7d8da..b7dcb9a5a 100644 --- a/services/core/tools/microsandbox-provider/main.go +++ b/services/core/tools/microsandbox-provider/main.go @@ -8,6 +8,7 @@ import ( "context" "crypto/sha256" "debug/elf" + _ "embed" "encoding/hex" "encoding/json" "errors" @@ -25,6 +26,9 @@ import ( sdk "github.com/superradcompany/microsandbox/sdk/go" ) +//go:embed go.mod +var moduleDefinition string + func main() { // The inherited node generation lease covers this helper's actual lifetime. // Set CLOEXEC before SDK initialization or any possible subprocess spawn so @@ -207,6 +211,16 @@ func allocationLock(q wire.Request) (*allocationGuard, error) { time.Sleep(20 * time.Millisecond) } } +func sdkModuleVersion() string { + for _, line := range strings.Split(moduleDefinition, "\n") { + fields := strings.Fields(line) + if len(fields) == 2 && fields[0] == "github.com/superradcompany/microsandbox/sdk/go" { + return fields[1] + } + } + return "" +} + func checkInstallation(c wire.Config) error { build, ok := debug.ReadBuildInfo() if !ok { @@ -215,7 +229,7 @@ func checkInstallation(c wire.Config) error { matched := false for _, d := range build.Deps { if d.Path == "github.com/superradcompany/microsandbox/sdk/go" { - matched = d.Version == wire.SDKVersion && d.Replace == nil + matched = d.Version == sdkModuleVersion() && d.Replace == nil } } if !matched { @@ -269,6 +283,13 @@ func checkInstallation(c wire.Config) error { if runtime.MSBPath != c.RuntimePath || runtime.LibkrunfwPath != c.FirmwarePath { return sandbox.ErrOwnership } + nativeVersion, e := sdk.RuntimeVersion() + if e != nil { + return e + } + if sdk.SDKVersion() != strings.TrimPrefix(wire.SDKVersion, "v") || nativeVersion != sdk.SDKVersion() { + return sandbox.ErrInvalid + } selected, e := sdk.DefaultBackendInfo() if e != nil { return e diff --git a/services/core/tools/microsandbox-provider/sdk_release_test.go b/services/core/tools/microsandbox-provider/sdk_release_test.go new file mode 100644 index 000000000..7d528949f --- /dev/null +++ b/services/core/tools/microsandbox-provider/sdk_release_test.go @@ -0,0 +1,28 @@ +//go:build linux + +package main + +import ( + "strings" + "testing" + + wire "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" + sdk "github.com/superradcompany/microsandbox/sdk/go" +) + +func TestOfficialSDKReleaseMatchesEmbeddedFFI(t *testing.T) { + t.Setenv("MSB_HOME", t.TempDir()) + if sdkModuleVersion() == "" { + t.Fatal("missing pinned SDK module identity") + } + if sdk.SDKVersion() != strings.TrimPrefix(wire.SDKVersion, "v") { + t.Fatal("SDK source does not match admitted native release") + } + version, err := sdk.RuntimeVersion() + if err != nil { + t.Fatal(err) + } + if version != sdk.SDKVersion() { + t.Fatalf("embedded FFI %q does not match SDK %q", version, sdk.SDKVersion()) + } +}