diff --git a/docs/en/runtime.md b/docs/en/runtime.md index 9e31f16..d156a07 100644 --- a/docs/en/runtime.md +++ b/docs/en/runtime.md @@ -123,10 +123,6 @@ Aliyun UID/AK/SK, and `DOCKER_IMAGE_BUILDER_USERNAME` / `DOCKER_IMAGE_BUILDER_PASSWORD` for registry auth unless the YAML overrides them under `cloudBuild.registry`. -When the CLI downloads docker-image-builder automatically, it verifies the -sibling `.sha256` file before caching or running the binary. Set -`DOCKER_IMAGE_BUILDER_BINPATH` to use an explicit local builder binary. - ### Examples ```bash diff --git a/docs/zh/runtime.md b/docs/zh/runtime.md index f3f0768..475e028 100644 --- a/docs/zh/runtime.md +++ b/docs/zh/runtime.md @@ -116,9 +116,6 @@ docker-image-builder;builder 成功退出时输出 `completed`。docker-image- 镜像仓库用户名和密码优先读 YAML 的 `cloudBuild.registry`,否则读取 `DOCKER_IMAGE_BUILDER_USERNAME` / `DOCKER_IMAGE_BUILDER_PASSWORD`。 -CLI 自动下载 docker-image-builder 时,会先校验同名 `.sha256` 文件,再缓存或执行该 -二进制。设置 `DOCKER_IMAGE_BUILDER_BINPATH` 可以使用显式指定的本地 builder。 - ### Examples ```bash diff --git a/src/agentrun_cli/_utils/cloud_build.py b/src/agentrun_cli/_utils/cloud_build.py index 723adc7..8faa676 100644 --- a/src/agentrun_cli/_utils/cloud_build.py +++ b/src/agentrun_cli/_utils/cloud_build.py @@ -16,8 +16,28 @@ from agentrun_cli._utils.agentruntime_yaml import ParsedAgentRuntime, ParsedCloudBuild -BUILDER_RELEASE_TAG = "latest" +BUILDER_RELEASE_TAG = "v0.0.0-20260518-164317-160dd89efac1" BUILDER_BASE_URL = "https://images.devsapp.cn/docker-image-builder" +BUILDER_RELEASE_SHA256 = { + "docker-image-builder-darwin-amd64": ( + "7311df3d1026a5a66823da951c05b9ec455e2d8514af76aeb550d97e3629cb5e" + ), + "docker-image-builder-darwin-arm64": ( + "c6112ac61d85815e8103ed21989c40828a49798178b73337cb957d9ff433c338" + ), + "docker-image-builder-linux-amd64": ( + "ad8af2d620f0509b20cef2967fc296915b85a28ebb40a16116b64b41d67820e2" + ), + "docker-image-builder-linux-arm64": ( + "ec4cf574ce43051e04f45eeedf2a2622c79d75c62efda9b353578898b0bed714" + ), + "docker-image-builder-windows-amd64.exe": ( + "cb19f3af6613eba2f42e31d925238b7ef7847fcaa615247fbe4fc179b80a23e7" + ), + "docker-image-builder-windows-arm64.exe": ( + "7a44d8f36ba30c140700ce5b8dcf8dce78b1e9e71e6583b0b8f07c8fd8e5d6b4" + ), +} class CloudBuildError(RuntimeError): @@ -213,7 +233,7 @@ def ensure_builder_binary() -> str: artifact = _artifact_name() url = f"{BUILDER_BASE_URL}/{tag}/{artifact}" try: - expected_sha256 = _download_sha256(f"{url}.sha256", artifact) + expected_sha256 = _expected_sha256(tag, url, artifact) if _is_executable(target) and _sha256_file(target) == expected_sha256: return str(target) _download_binary(url, tmp) @@ -226,6 +246,31 @@ def ensure_builder_binary() -> str: return str(target) +def _expected_sha256(tag: str, url: str, artifact_name: str) -> str: + """Return the expected SHA256 digest for a release artifact. + + Args: + tag: Release tag being installed. + url: Artifact download URL. + artifact_name: Expected release artifact name. + """ + if tag == BUILDER_RELEASE_TAG: + return _pinned_sha256(artifact_name) + return _download_sha256(f"{url}.sha256", artifact_name) + + +def _pinned_sha256(artifact_name: str) -> str: + """Return the pinned release SHA256 digest for an artifact. + + Args: + artifact_name: Expected release artifact name. + """ + digest = BUILDER_RELEASE_SHA256.get(artifact_name) + if not digest: + raise CloudBuildError(f"missing pinned sha256 for {artifact_name}") + return digest + + def _download_binary(url: str, target: Path) -> None: """Download a binary to a temporary local path. diff --git a/tests/unit/test_cloud_build.py b/tests/unit/test_cloud_build.py index 7d059e4..31f04a1 100644 --- a/tests/unit/test_cloud_build.py +++ b/tests/unit/test_cloud_build.py @@ -18,6 +18,7 @@ ParsedContainer, ) from agentrun_cli._utils.cloud_build import ( + BUILDER_RELEASE_SHA256, BUILDER_RELEASE_TAG, CloudBuildError, build_builder_args, @@ -159,7 +160,14 @@ def test_ensure_builder_binary_rejects_bad_binpath(monkeypatch, tmp_path): ensure_builder_binary() -def test_ensure_builder_binary_downloads_latest_with_checksum(monkeypatch, tmp_path): +def test_builder_release_tag_is_pinned(): + assert BUILDER_RELEASE_TAG.startswith("v0.0.0-") + assert BUILDER_RELEASE_TAG != "latest" + + +def test_ensure_builder_binary_downloads_pinned_version_with_checksum( + monkeypatch, tmp_path +): monkeypatch.delenv("DOCKER_IMAGE_BUILDER_BINPATH", raising=False) monkeypatch.delenv("DOCKER_IMAGE_BUILDER_BINTAG", raising=False) monkeypatch.setenv("HOME", str(tmp_path)) @@ -173,18 +181,18 @@ def fake_download(url, target): assert f"/{BUILDER_RELEASE_TAG}/" in url target.write_bytes(content) - def fake_download_sha256(url, artifact_name): - assert url.endswith("/docker-image-builder-linux-amd64.sha256") - assert artifact_name == "docker-image-builder-linux-amd64" - return sha256(content).hexdigest() - + monkeypatch.setitem( + BUILDER_RELEASE_SHA256, + "docker-image-builder-linux-amd64", + sha256(content).hexdigest(), + ) monkeypatch.setattr( "agentrun_cli._utils.cloud_build._download_binary", fake_download, ) monkeypatch.setattr( "agentrun_cli._utils.cloud_build._download_sha256", - fake_download_sha256, + lambda *_args: pytest.fail("pinned release should use embedded checksum"), ) binary = ensure_builder_binary() expected_suffix = ( @@ -214,7 +222,45 @@ def test_ensure_builder_binary_uses_cached_bintag(monkeypatch, tmp_path): assert ensure_builder_binary() == str(cached) -def test_ensure_builder_binary_replaces_stale_cached_latest(monkeypatch, tmp_path): +def test_ensure_builder_binary_downloads_custom_bintag_with_remote_checksum( + monkeypatch, tmp_path +): + monkeypatch.delenv("DOCKER_IMAGE_BUILDER_BINPATH", raising=False) + monkeypatch.setenv("DOCKER_IMAGE_BUILDER_BINTAG", "custom-tag") + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setattr( + "agentrun_cli._utils.cloud_build._artifact_name", + lambda: "docker-image-builder-linux-amd64", + ) + content = b"custom" + + def fake_download(url, target): + assert "/custom-tag/" in url + target.write_bytes(content) + + def fake_download_sha256(url, artifact_name): + assert url.endswith("/custom-tag/docker-image-builder-linux-amd64.sha256") + assert artifact_name == "docker-image-builder-linux-amd64" + return sha256(content).hexdigest() + + monkeypatch.setattr( + "agentrun_cli._utils.cloud_build._download_binary", + fake_download, + ) + monkeypatch.setattr( + "agentrun_cli._utils.cloud_build._download_sha256", + fake_download_sha256, + ) + + binary = ensure_builder_binary() + + assert binary.endswith(".docker-image-builder/custom-tag/docker-image-builder") + assert os.access(binary, os.X_OK) + + +def test_ensure_builder_binary_replaces_stale_cached_pinned_release( + monkeypatch, tmp_path +): monkeypatch.delenv("DOCKER_IMAGE_BUILDER_BINPATH", raising=False) monkeypatch.delenv("DOCKER_IMAGE_BUILDER_BINTAG", raising=False) monkeypatch.setenv("HOME", str(tmp_path)) @@ -232,9 +278,10 @@ def test_ensure_builder_binary_replaces_stale_cached_latest(monkeypatch, tmp_pat cached.write_bytes(b"old") cached.chmod(cached.stat().st_mode | stat.S_IXUSR) new_content = b"new" - monkeypatch.setattr( - "agentrun_cli._utils.cloud_build._download_sha256", - lambda *_args: sha256(new_content).hexdigest(), + monkeypatch.setitem( + BUILDER_RELEASE_SHA256, + "docker-image-builder-linux-amd64", + sha256(new_content).hexdigest(), ) def fake_download(_url, target): @@ -256,9 +303,10 @@ def test_ensure_builder_binary_rejects_checksum_mismatch(monkeypatch, tmp_path): "agentrun_cli._utils.cloud_build._artifact_name", lambda: "docker-image-builder-linux-amd64", ) - monkeypatch.setattr( - "agentrun_cli._utils.cloud_build._download_sha256", - lambda *_args: sha256(b"expected").hexdigest(), + monkeypatch.setitem( + BUILDER_RELEASE_SHA256, + "docker-image-builder-linux-amd64", + sha256(b"expected").hexdigest(), ) monkeypatch.setattr( "agentrun_cli._utils.cloud_build._download_binary", @@ -276,9 +324,10 @@ def test_ensure_builder_binary_download_failure(monkeypatch, tmp_path): "agentrun_cli._utils.cloud_build._artifact_name", lambda: "docker-image-builder-linux-amd64", ) - monkeypatch.setattr( - "agentrun_cli._utils.cloud_build._download_sha256", - lambda *_args: sha256(b"bin").hexdigest(), + monkeypatch.setitem( + BUILDER_RELEASE_SHA256, + "docker-image-builder-linux-amd64", + sha256(b"bin").hexdigest(), ) monkeypatch.setattr( "agentrun_cli._utils.cloud_build._download_binary", @@ -329,6 +378,11 @@ def read(self): ) +def test_pinned_sha256_rejects_unknown_artifact(): + with pytest.raises(CloudBuildError, match="missing pinned sha256"): + cloud_build_mod._pinned_sha256("docker-image-builder-plan9-amd64") + + def test_parse_sha256_accepts_raw_digest(): digest = "a" * 64 assert cloud_build_mod._parse_sha256(digest, "artifact") == digest