From e511295023034aa5417df4aad592e8ededa0e333 Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 8 Jun 2026 09:41:31 -0300 Subject: [PATCH] Use canonical builder source hash --- agent_builder/tools.py | 17 ++++++++++++++++- tests/test_template_init.py | 28 +++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/agent_builder/tools.py b/agent_builder/tools.py index ed92c5c..2494865 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -1278,7 +1278,22 @@ def _write_builder_state( def _source_bundle_hash(bundle: bytes) -> str: - return hashlib.sha256(bundle).hexdigest() + digest = hashlib.sha256() + with tarfile.open(fileobj=io.BytesIO(bundle), mode="r:gz") as tf: + members = sorted( + (member for member in tf.getmembers() if member.isfile()), + key=lambda member: member.name, + ) + for member in members: + extracted = tf.extractfile(member) + body = extracted.read() if extracted is not None else b"" + digest.update(member.name.encode("utf-8")) + digest.update(b"\0") + digest.update(str(len(body)).encode("ascii")) + digest.update(b"\0") + digest.update(body) + digest.update(b"\0") + return digest.hexdigest() def _record_sandbox_result( diff --git a/tests/test_template_init.py b/tests/test_template_init.py index bb430fb..79cb620 100644 --- a/tests/test_template_init.py +++ b/tests/test_template_init.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -import hashlib +import gzip import io import json import tarfile @@ -22,6 +22,7 @@ from agent_builder.tools import ( _replace_workspace_files, _render_a2a_init_template, _render_skill_md, + _source_bundle_hash, _tarball_workspace_dir, ToolContext, build_tools, @@ -382,9 +383,9 @@ class TemplateInitTests(unittest.TestCase): } for rel, content in files.items(): workspace.write_bytes(prefix + rel, content.encode("utf-8")) - source_hash = hashlib.sha256( + source_hash = _source_bundle_hash( _tarball_workspace_dir(_WorkspaceObjectStore(workspace), prefix) - ).hexdigest() + ) workspace.write_bytes( prefix + _BUILDER_STATE_FILE, json.dumps({ @@ -478,6 +479,27 @@ class TemplateInitTests(unittest.TestCase): self.assertEqual(result["error"], "sandbox_not_passed") self.assertEqual(_FakeAsyncClient.posts, []) + def test_source_bundle_hash_ignores_archive_metadata(self) -> None: + def bundle_with_mtime(mtime: int) -> bytes: + raw = io.BytesIO() + with tarfile.open(fileobj=raw, mode="w") as tf: + body = b"print('ok')\n" + info = tarfile.TarInfo(name="agent.py") + info.size = len(body) + info.mode = 0o644 + info.mtime = mtime + tf.addfile(info, io.BytesIO(body)) + compressed = io.BytesIO() + with gzip.GzipFile(fileobj=compressed, mode="wb", mtime=mtime) as gz: + gz.write(raw.getvalue()) + return compressed.getvalue() + + first = bundle_with_mtime(1) + second = bundle_with_mtime(2) + + self.assertNotEqual(first, second) + self.assertEqual(_source_bundle_hash(first), _source_bundle_hash(second)) + def test_cp_compose_meta_agent_posts_manifest_and_records_state(self) -> None: workspace = _FakeWorkspace() tools = build_tools(