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
4 changes: 4 additions & 0 deletions .github/workflows/release_agent_email.yml
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,9 @@ jobs:
npm run build

- name: Assemble + publish the whole-package zip (all platforms + client + docs)
# DISABLED: the ~177 MB all-platforms zip exceeds Cloudflare's edge upload
# limit (POST returns 413). Revive via presigned-to-R2 or per-platform zips.
if: false
shell: bash
env:
AGENT_HUB_PUBLISH_TOKEN: ${{ secrets.GAIA_HUB_TOKEN }}
Expand Down Expand Up @@ -564,6 +567,7 @@ jobs:
done

- name: Verify the published package zip is fetchable at the edge
if: false # disabled together with the whole-package zip publish above
shell: bash
env:
GAIA_HUB_BASE_URL: ${{ vars.GAIA_HUB_BASE_URL }}
Expand Down
10 changes: 10 additions & 0 deletions hub/agents/npm/agent-email/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ follows [SemVer](https://semver.org/): the **MAJOR** of the on-the-wire
`SCHEMA_VERSION` is what `checkVersion` enforces at startup, so a contract MAJOR
bump is always at least a package MINOR bump with a migration note.

## 0.2.4

First fully-published release of this feature set. The whole-package zip download
(#1843) is temporarily disabled: the ~177 MB all-platforms zip exceeds Cloudflare's
edge upload limit, so the publish step rejected it (413) and blocked every prior
attempt (0.2.1–0.2.3). The worker-side streaming approach was reverted; 0.2.4 ships
the per-platform binaries + this npm client without the combined zip. Per-platform
binaries remain individually downloadable from the Hub. No agent wire-contract
change — `SCHEMA_VERSION` stays `2.0`.

## 0.2.3

Re-cut of 0.2.2 after the Agent Hub worker was redeployed with the large-artifact
Expand Down
2 changes: 1 addition & 1 deletion hub/agents/npm/agent-email/assets/architecture.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
<div class="card" id="card">
<div class="head">
<span class="pkg">@amd-gaia/agent-email</span>
<span class="ver" id="ver">v0.2.3</span>
<span class="ver" id="ver">v0.2.4</span>
<span class="tag">architecture</span>
</div>
<div class="install"><span class="p mono">$</span><code>npm i @amd-gaia/agent-email</code></div>
Expand Down
4 changes: 2 additions & 2 deletions hub/agents/npm/agent-email/binaries.lock.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schemaVersion": "1.0",
"agentVersion": "0.2.3",
"baseUrl": "https://hub.amd-gaia.ai/agents/email/0.2.3",
"agentVersion": "0.2.4",
"baseUrl": "https://hub.amd-gaia.ai/agents/email/0.2.4",
"binaries": {
"win32-x64": {
"filename": "email-agent-win32-x64.exe",
Expand Down
2 changes: 1 addition & 1 deletion hub/agents/npm/agent-email/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@amd-gaia/agent-email",
"version": "0.2.3",
"version": "0.2.4",
"type": "module",
"description": "Thin JS/TS client + build-time binary fetcher + sidecar lifecycle helpers for the GAIA email agent (frozen, no-Python REST sidecar). Runs 100% locally on AMD Ryzen AI.",
"author": "AMD AI Group",
Expand Down
2 changes: 1 addition & 1 deletion hub/agents/python/email/gaia-agent.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: email
name: Email Triage
version: 0.2.3
version: 0.2.4
description: "GAIA email triage agent — read, triage, organize, and reply to Gmail/Outlook locally"
author: AMD
license: MIT
Expand Down
2 changes: 1 addition & 1 deletion hub/agents/python/email/gaia_agent_email/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# Package build version. Keep in sync with ``pyproject.toml``'s ``version`` —
# ``test_rest_contract.test_agent_version_matches_package_metadata`` asserts the
# installed distribution metadata agrees with this literal so the two never drift.
AGENT_VERSION = "0.2.3"
AGENT_VERSION = "0.2.4"

# REST/contract version exposed to hosts. Aliased to the frozen contract's
# SCHEMA_VERSION so a contract bump is an API bump — no second number to forget.
Expand Down
157 changes: 14 additions & 143 deletions hub/agents/python/email/packaging/publish_to_r2.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
from __future__ import annotations

import argparse
import base64
import hashlib
import json
import os
Expand All @@ -54,21 +53,11 @@

PUBLISH_PATH = "/publish"
TOKEN_ENV = "AGENT_HUB_PUBLISH_TOKEN"
_CHUNK = 1 << 20 # 1 MiB read chunks for streaming sha256


def _sha256_file(path: Path) -> tuple[str, int]:
"""Hash a file in chunks — never loads the full content into memory."""
h = hashlib.sha256()
size = 0
with path.open("rb") as fh:
while True:
chunk = fh.read(_CHUNK)
if not chunk:
break
h.update(chunk)
size += len(chunk)
return h.hexdigest(), size
data = path.read_bytes()
return hashlib.sha256(data).hexdigest(), len(data)


def _read_token() -> str:
Expand Down Expand Up @@ -129,109 +118,6 @@ def _download_sha256(base_url: str, agent_id: str, version: str, filename: str)
return hashlib.sha256(resp.content).hexdigest()


def _b64(data: bytes) -> str:
"""Base64-encode bytes to ASCII string."""
return base64.b64encode(data).decode("ascii")


def publish_streaming(
base_url: str,
manifest_path: Path,
manifest: dict,
artifact_path: Path,
platform_key: str,
token: str,
package_files_bytes: bytes | None = None,
) -> dict:
"""Stream the artifact as a raw request body (application/octet-stream).

Used for the whole-package zip (~177 MB) to avoid buffering the full file
in memory, which would exceed Cloudflare's ~128 MB per-Worker memory limit.
Metadata travels in X-Gaia-* headers. The file handle is passed directly to
requests so it streams chunk-by-chunk without loading the full content.
"""
if not artifact_path.exists():
raise SystemExit(f"error: artifact not found: {artifact_path}")
filename = artifact_path.name
local_sha, size = _sha256_file(artifact_path)
agent_id = str(manifest["id"])
version = str(manifest["version"])
publish_url = f"{base_url.rstrip('/')}{PUBLISH_PATH}"

print(
f"[publish] {filename} ({size} bytes, sha256={local_sha[:12]}…) "
f"-> {agent_id}@{version} [streaming]",
flush=True,
)

manifest_bytes = manifest_path.read_bytes()
headers = {
"authorization": f"Bearer {token}",
"content-type": "application/octet-stream",
"content-length": str(size),
# Stored object content-type (the package artifact is a zip). The
# request body itself is octet-stream; this is the metadata the Worker
# persists on the R2 object.
"x-gaia-content-type": "application/zip",
"x-gaia-manifest": _b64(manifest_bytes),
"x-gaia-filename": filename,
"x-gaia-sha256": local_sha,
}
if package_files_bytes is not None:
headers["x-gaia-package-files"] = _b64(package_files_bytes)

with artifact_path.open("rb") as fh:
resp = requests.post(
publish_url,
headers=headers,
data=fh, # streams chunk-by-chunk; never loads full file
timeout=600,
)

if resp.status_code == 201:
body = resp.json()
server_sha = body.get("published", {}).get("artifact", {}).get("sha256")
if server_sha != local_sha:
raise SystemExit(
f"error: integrity check FAILED for {filename}: Worker stored "
f"sha256={server_sha} but local sha256={local_sha}. The upload was "
"corrupted in transit; failing loudly."
)
n = body.get("published", {}).get("version_artifacts", "?")
print(
f"[publish] OK 201 — stored, server sha256 verified. "
f"{agent_id}@{version} now has {n} artifact(s).",
flush=True,
)
elif resp.status_code == 409:
remote_sha = _download_sha256(base_url, agent_id, version, filename)
if remote_sha != local_sha:
raise SystemExit(
f"error: {filename} is already published at {agent_id}@{version} "
f"with a DIFFERENT sha256 (remote={remote_sha}, local={local_sha}). "
"Published artifacts are immutable — bump the version to change it."
)
print(
f"[publish] OK 409 — already published with identical bytes "
f"(idempotent no-op).",
flush=True,
)
else:
raise SystemExit(
f"error: publish of {filename} failed: HTTP {resp.status_code} "
f"{resp.text[:500]}"
)

executable = "email-agent.exe" if filename.endswith(".exe") else "email-agent"
return {
"platform": platform_key,
"filename": filename,
"executable": executable,
"sha256": local_sha,
"size": size,
}


def publish_one(
base_url: str,
manifest_path: Path,
Expand Down Expand Up @@ -432,34 +318,19 @@ def main(argv=None) -> int:
if path.name.lower().endswith(".zip")
else _infer_platform_key(path.name)
)
if platform_key == "package":
# Whole-package zip: stream directly to R2 to avoid buffering ~177 MB
# in memory (Cloudflare Workers hard ~128 MB per-request limit).
results.append(
publish_streaming(
args.base_url,
args.manifest,
manifest,
path,
platform_key,
token,
package_files_bytes=package_files_bytes,
)
)
else:
results.append(
publish_one(
args.base_url,
args.manifest,
manifest,
path,
platform_key,
token,
readme_bytes=readme_bytes,
changelog_bytes=changelog_bytes,
package_files_bytes=package_files_bytes,
)
results.append(
publish_one(
args.base_url,
args.manifest,
manifest,
path,
platform_key,
token,
readme_bytes=readme_bytes,
changelog_bytes=changelog_bytes,
package_files_bytes=package_files_bytes,
)
)

if args.summary_out:
args.summary_out.write_text(json.dumps(results, indent=2), encoding="utf-8")
Expand Down
2 changes: 1 addition & 1 deletion hub/agents/python/email/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "gaia-agent-email"
version = "0.2.3"
version = "0.2.4"
description = "GAIA email triage agent — read, triage, organize, and reply to Gmail/Outlook locally"
authors = [{ name = "AMD" }]
license = { text = "MIT" }
Expand Down
Loading
Loading