fix: reject stale agent scaffolds
All checks were successful
build / build (push) Successful in 2s

This commit is contained in:
robert
2026-05-17 18:17:56 -03:00
parent 83e3ad3c3b
commit aa5ee5b0fe
2 changed files with 81 additions and 1 deletions

View File

@@ -213,6 +213,16 @@ def build_tools(ctx: ToolContext) -> list[Any]:
return json.dumps({ return json.dumps({
"error": "no files in agents/" + name + "/ — write some first", "error": "no files in agents/" + name + "/ — write some first",
}) })
try:
_assert_current_agent_project(bundle_bytes)
except RuntimeError as exc:
return json.dumps({
"error": str(exc),
"hint": (
"Call init_agent_template first, then edit the generated "
"DeepAgents files instead of replacing agent.py from memory."
),
})
b64 = base64.b64encode(bundle_bytes).decode("ascii") b64 = base64.b64encode(bundle_bytes).decode("ascii")
script = ( script = (
"set -e\n" "set -e\n"
@@ -279,6 +289,16 @@ def build_tools(ctx: ToolContext) -> list[Any]:
bundle_bytes = _tarball_workspace_dir(s3, bucket, prefix) bundle_bytes = _tarball_workspace_dir(s3, bucket, prefix)
if not bundle_bytes: if not bundle_bytes:
return json.dumps({"error": f"no files at agents/{name}/"}) return json.dumps({"error": f"no files at agents/{name}/"})
try:
_assert_current_agent_project(bundle_bytes)
except RuntimeError as exc:
return json.dumps({
"error": str(exc),
"hint": (
"Run init_agent_template again or restore the generated "
"DeepAgents scaffold before deploying."
),
})
# Read the entrypoint from a2a.yaml so we can pass it verbatim. # Read the entrypoint from a2a.yaml so we can pass it verbatim.
entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}" entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}"
@@ -508,6 +528,32 @@ def _read_entrypoint(bundle: bytes) -> str | None:
return None return None
def _read_bundle_text(bundle: bytes, path: str) -> str | None:
try:
with tarfile.open(fileobj=io.BytesIO(bundle), mode="r:gz") as tf:
for member in tf.getmembers():
if member.name == path and member.isfile():
extracted = tf.extractfile(member)
if extracted is None:
return None
return extracted.read().decode("utf-8", "replace")
except Exception: # noqa: BLE001
return None
return None
def _assert_current_agent_project(bundle: bytes) -> None:
files = {
path: text
for path in ("agent.py", "requirements.txt")
if (text := _read_bundle_text(bundle, path)) is not None
}
_assert_current_deepagents_scaffold(
files,
source="agent project",
)
def _class_name(slug: str) -> str: def _class_name(slug: str) -> str:
return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent" return "".join(p.capitalize() for p in re.split(r"[-_]+", slug) if p) or "MyAgent"
@@ -538,6 +584,17 @@ def _render_a2a_init_template(
def _assert_current_a2a_init_template(files: dict[str, str]) -> None: def _assert_current_a2a_init_template(files: dict[str, str]) -> None:
_assert_current_deepagents_scaffold(
files,
source="installed a2a-pack init template",
)
def _assert_current_deepagents_scaffold(
files: dict[str, str],
*,
source: str,
) -> None:
required_markers = { required_markers = {
"agent.py": ( "agent.py": (
"LLMProvisioning.CALLER_PROVIDED", "LLMProvisioning.CALLER_PROVIDED",
@@ -567,7 +624,7 @@ def _assert_current_a2a_init_template(files: dict[str, str]) -> None:
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
version = "unknown" version = "unknown"
raise RuntimeError( raise RuntimeError(
"installed a2a-pack init template is stale; expected the 0.1.3+ " f"{source} is stale; expected the 0.1.3+ "
"DeepAgents caller-LLM scaffold. " "DeepAgents caller-LLM scaffold. "
f"a2a_pack={version}; missing markers: {', '.join(missing)}" f"a2a_pack={version}; missing markers: {', '.join(missing)}"
) )

View File

@@ -1,8 +1,11 @@
from __future__ import annotations from __future__ import annotations
import io
import tarfile
import unittest import unittest
from agent_builder.tools import ( from agent_builder.tools import (
_assert_current_agent_project,
_assert_current_a2a_init_template, _assert_current_a2a_init_template,
_render_a2a_init_template, _render_a2a_init_template,
) )
@@ -35,3 +38,23 @@ class TemplateInitTests(unittest.TestCase):
"agent.py": "from a2a_pack import A2AAgent", "agent.py": "from a2a_pack import A2AAgent",
"requirements.txt": "httpx>=0.27", "requirements.txt": "httpx>=0.27",
}) })
def test_stale_agent_project_is_rejected(self) -> None:
bundle = _tarball({
"agent.py": "from a2a_pack import A2AAgent\n",
"requirements.txt": "httpx>=0.27\n",
})
with self.assertRaisesRegex(RuntimeError, "agent project is stale"):
_assert_current_agent_project(bundle)
def _tarball(files: dict[str, str]) -> bytes:
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
for path, text in files.items():
body = text.encode("utf-8")
info = tarfile.TarInfo(path)
info.size = len(body)
tf.addfile(info, io.BytesIO(body))
return buf.getvalue()