Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ message attachments and exposes files shared by the agent through
the runtime: local shared resources expire with the retained session. The limits
are 50 MiB per file, 64 MiB and 128 shared files per session.

## Runtime release maintenance

The SDK runtime manifest is maintained independently of schema generation. After
publishing and verifying a compatible runtime, run `scripts/bump_runtime.py` with
`--version` and one `--sha PLATFORM=SHA256` for every platform already in the
manifest. Partial updates are rejected so a new URL cannot retain an old digest.
Schema generation never touches it. The release workflow still opens
legacy CLI pin PRs; retarget it only after the SDK/CLI migration has shipped.

## How a session works

A session is one run of an agent against a task. It moves through a small set of states: `pending`, `running`, and then a settled state such as `completed`, `idle`, `failed`, `timed_out`, or `interrupted`.
Expand Down
102 changes: 102 additions & 0 deletions scripts/bump_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Rewrite the pinned hai-agent-runtime version + per-platform sha256 in the SDK runtime manifest."""

from __future__ import annotations

import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path

# Stdlib only on purpose: this runs as `python scripts/bump_runtime.py` in a
# checkout with no dependencies installed, so a third-party import would break
# the bump step.
RUNTIME_INSTALL = Path(__file__).parents[1] / "src" / "hai_agents_local" / "runtime" / "manifest.py"
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
_MANIFEST_FILENAME_RE = re.compile(r'"hai-agent-runtime-([^".]+)\.zip"')


@dataclass(frozen=True)
class RuntimeBump:
version: str
shas: dict[str, str] # platform key (e.g. darwin-arm64) -> sha256 hex

def __post_init__(self) -> None:
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?", self.version):
raise ValueError("runtime version must be a release version, e.g. 0.1.13")
for platform, sha in self.shas.items():
if not _SHA256_RE.fullmatch(sha) or sha == "0" * 64:
raise ValueError(f"{platform}: {sha!r} is not a lowercase 64-char sha256")


def _filename_for(platform: str) -> str:
return f"hai-agent-runtime-{platform}.zip"


def _manifest_platforms(source: str) -> set[str]:
"""Platform keys that have a published artifact literal in `source`."""
return set(_MANIFEST_FILENAME_RE.findall(source))


def apply_bump(source: str, bump: RuntimeBump) -> str:
"""Return `source` with PINNED_RUNTIME_VERSION and the manifest digests replaced; raises if any anchor is missing."""
published = _manifest_platforms(source)
extra = bump.shas.keys() - published
if extra:
raise ValueError(f"no manifest entry for platform(s): {sorted(extra)}")
# The version is a single literal feeding every derived URL, so any published
# platform left without a fresh sha would keep a stale digest at the new
# version's URL and fail verification on download. Refuse the partial bump.
missing = published - bump.shas.keys()
if missing:
raise ValueError(f"missing sha for published platform(s): {sorted(missing)}")

updated, count = re.subn(
r'PINNED_RUNTIME_VERSION = "[^"]*"',
f'PINNED_RUNTIME_VERSION = "{bump.version}"',
source,
)
if count != 1:
raise ValueError(f"expected exactly one PINNED_RUNTIME_VERSION assignment, found {count}")

for platform, sha in bump.shas.items():
filename = _filename_for(platform)
pattern = re.compile(rf'("{re.escape(filename)}",\s*(?:#[^\n]*\n\s*)*")[0-9a-fA-F]{{64}}(")')
updated, count = pattern.subn(rf"\g<1>{sha}\g<2>", updated)
if count != 1:
raise ValueError(f"expected exactly one sha256 literal for {filename}, found {count}")
return updated


def _parse_args(argv: list[str]) -> RuntimeBump:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument(
"--sha",
action="append",
required=True,
metavar="PLATFORM=SHA256",
help="per-platform digest, e.g. darwin-arm64=<hex> (repeatable)",
)
args = parser.parse_args(argv)
shas: dict[str, str] = {}
for entry in args.sha:
platform, _, sha = entry.partition("=")
if not platform or not sha:
parser.error(f"--sha must be PLATFORM=SHA256, got {entry!r}")
if platform in shas:
parser.error(f"duplicate --sha for {platform}")
shas[platform] = sha.lower()
return RuntimeBump(version=args.version, shas=shas)


def main(argv: list[str]) -> int:
bump = _parse_args(argv)
source = RUNTIME_INSTALL.read_text()
RUNTIME_INSTALL.write_text(apply_bump(source, bump))
print(f"bumped runtime to {bump.version} ({', '.join(sorted(bump.shas))})")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
2 changes: 1 addition & 1 deletion src/hai_agents_local/runtime/manifest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Pinned hai-agent-runtime version and per-platform artifact digests.

This module is the SDK's single runtime pin: a runtime release bumps
PINNED_RUNTIME_VERSION and MANIFEST here and nothing else. Artifacts live under an
PINNED_RUNTIME_VERSION and MANIFEST here with scripts/bump_runtime.py. Artifacts live under an
immutable version-scoped CDN prefix, so an edge can never serve stale bytes.
"""

Expand Down
38 changes: 38 additions & 0 deletions tests/test_bump_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""The release updater must never move URLs while retaining a platform's old digest."""

import runpy
import shutil
import subprocess
import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]


@pytest.mark.parametrize("complete", [True, False])
def test_release_pin_update_is_complete_or_leaves_manifest_unchanged(tmp_path, complete):
script = tmp_path / "scripts" / "bump_runtime.py"
manifest = tmp_path / "src" / "hai_agents_local" / "runtime" / "manifest.py"
script.parent.mkdir(parents=True)
manifest.parent.mkdir(parents=True)
shutil.copyfile(ROOT / "scripts" / "bump_runtime.py", script)
shutil.copyfile(ROOT / "src" / "hai_agents_local" / "runtime" / "manifest.py", manifest)
before = manifest.read_bytes()
original = runpy.run_path(str(manifest))["MANIFEST"]
shas = {platform: f"{index:064x}" for index, platform in enumerate(original, start=1)}
args = [sys.executable, str(script), "--version", "9.8.7"]
for platform, sha in list(shas.items())[: len(shas) if complete else -1]:
args.extend(["--sha", f"{platform}={sha}"])
result = subprocess.run(args, capture_output=True, text=True)
if not complete:
assert result.returncode != 0
assert manifest.read_bytes() == before
return
assert result.returncode == 0, result.stderr
updated = runpy.run_path(str(manifest))["MANIFEST"]
assert set(updated) == set(original)
for platform, artifact in updated.items():
assert artifact.sha256 == shas[platform]
assert artifact.url.endswith(f"/9.8.7/hai-agent-runtime-{platform}.zip")
Loading