Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/en/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions docs/zh/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 47 additions & 2 deletions src/agentrun_cli/_utils/cloud_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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.

Expand Down
88 changes: 71 additions & 17 deletions tests/unit/test_cloud_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ParsedContainer,
)
from agentrun_cli._utils.cloud_build import (
BUILDER_RELEASE_SHA256,
BUILDER_RELEASE_TAG,
CloudBuildError,
build_builder_args,
Expand Down Expand Up @@ -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))
Expand All @@ -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 = (
Expand Down Expand Up @@ -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))
Expand All @@ -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):
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
Loading