From f4d0b3eeba0ccbf9ead2e67121e87fe04a414015 Mon Sep 17 00:00:00 2001 From: Cynthia Wang Date: Fri, 28 Aug 2026 13:25:50 -0400 Subject: [PATCH 1/3] feat(tracing): add opt-in commit SHA stamping for SGP spans Answers "which commit handled this request?" from the SGP Traces UI: search `__commit_sha__:` and read it in the span-detail Metadata panel. Agent spans never reach the OTel collector -- the SDK POSTs them straight to SGP -- so span metadata is the only carrier, and the value has to be present in the agent's own process. Opt-in, modelled on the lineage registry next door: the module global's default is the off state, so `commit_sha()` returns None and the stamp site needs no flag. Nothing is emitted until an agent calls `adk.code_revision.enable()`, and no agent inherits the field by upgrading the SDK. Deliberately separate from the automatic `__agent_version__`, which carries the deployed image tag verbatim. That tag is a real commit on GCP/Azure CI but an "-" composite on AWS ECR, "latest" on local deploys, and an arbitrary string on manual dispatch -- so a field named for a commit must not mirror it. Values that are not git object names are refused with a warning, and `__commit_sha__` therefore only ever holds one. Value precedence: explicit argument, then AGENT_COMMIT_SHA, then AGENT_VERSION only when it is already SHA-shaped -- so a platform-deployed agent that opts in needs no extra plumbing, while a custom build path bakes AGENT_COMMIT_SHA into the image. Stamped in the SGP processor rather than at span creation. That scopes it to the SGP backend, which is the user story here (and 92 of the fleet's agents), and it re-runs at span end, so the value survives an agent replacing span.data mid-span -- which real agent code does today. Co-Authored-By: Claude Opus 5 --- src/agentex/lib/adk/__init__.py | 4 + src/agentex/lib/core/tracing/code_revision.py | 105 +++++++++++++++++ .../processors/sgp_tracing_processor.py | 6 + src/agentex/lib/environment_variables.py | 7 ++ .../processors/test_sgp_tracing_processor.py | 29 +++++ tests/lib/core/tracing/test_code_revision.py | 109 ++++++++++++++++++ 6 files changed, 260 insertions(+) create mode 100644 src/agentex/lib/core/tracing/code_revision.py create mode 100644 tests/lib/core/tracing/test_code_revision.py diff --git a/src/agentex/lib/adk/__init__.py b/src/agentex/lib/adk/__init__.py index d5be0ac52..c05f8f3ea 100644 --- a/src/agentex/lib/adk/__init__.py +++ b/src/agentex/lib/adk/__init__.py @@ -31,6 +31,9 @@ # Data-source refs for lineage (SGP-6513); implementation lives in core.tracing from agentex.lib.core.tracing import lineage + +# Opt-in commit-SHA stamping (AGX1-969); implementation in core.tracing +from agentex.lib.core.tracing import code_revision from agentex.lib.core.tracing.lineage import DataSourceRef, data_sources # Unified harness surface (AGX1-375) @@ -73,6 +76,7 @@ "TurnSpan", # Lineage data-source refs (SGP-6513) "lineage", + "code_revision", "DataSourceRef", "data_sources", # Checkpointing / LangGraph diff --git a/src/agentex/lib/core/tracing/code_revision.py b/src/agentex/lib/core/tracing/code_revision.py new file mode 100644 index 000000000..7b08dd45f --- /dev/null +++ b/src/agentex/lib/core/tracing/code_revision.py @@ -0,0 +1,105 @@ +"""Opt-in stamping of the agent's source commit onto its spans. + +Nothing is stamped until the agent calls :func:`enable`, mirroring the +``lineage`` registry next door: a process-wide switch the agent sets once at +import, rather than automatic behaviour every agent inherits. When enabled the +resolved commit lands in span data under ``__commit_sha__`` and is searchable in +the SGP Traces UI as ``__commit_sha__:``. + +This is deliberately separate from ``__agent_version__``, which is automatic and +carries the deployed image tag verbatim ("image tag or git sha"). That tag is a +real commit on some build paths but an ``-`` composite (AWS +ECR), ``latest``, or a hand-passed tag on others -- so a field named for a commit +must not simply mirror it. Values that are not git object names are refused, and +a field named ``__commit_sha__`` therefore only ever holds one. +""" + +from __future__ import annotations + +import os +import re + +from agentex.lib.utils.logging import make_logger + +__all__ = ("COMMIT_SHA_KEY", "enable", "disable", "is_enabled", "commit_sha") + +logger = make_logger(__name__) + +COMMIT_SHA_KEY = "__commit_sha__" + +# A git object name: 40 hex for SHA-1, 64 for SHA-256, or an abbreviation down to +# git's own 7-character minimum. +_GIT_SHA_RE = re.compile(r"[0-9a-fA-F]{7,64}") + +_COMMIT_SHA_ENV = "AGENT_COMMIT_SHA" +# Fallback only: automatic, and only usable when it happens to be SHA-shaped. +_AGENT_VERSION_ENV = "AGENT_VERSION" + +# Resolved once at enable() rather than per span: the value is fixed for the +# life of the process, and resolving eagerly means a bad value is reported at +# startup instead of silently producing unstamped spans. +_commit_sha: str | None = None + + +def enable(commit_sha: str | None = None) -> None: + """Opt this process in to stamping ``__commit_sha__`` onto every span. + + Value precedence: the explicit ``commit_sha`` argument, else + ``AGENT_COMMIT_SHA``, else ``AGENT_VERSION`` when the deployment happened to + set it to a bare commit SHA. A value that is not a git object name is + refused with a warning and leaves stamping off -- better an absent field + than one named for a commit that holds an image tag. + """ + global _commit_sha + + for value, source in ( + (commit_sha, "the commit_sha argument"), + (os.environ.get(_COMMIT_SHA_ENV), _COMMIT_SHA_ENV), + (os.environ.get(_AGENT_VERSION_ENV), _AGENT_VERSION_ENV), + ): + candidate = (value or "").strip() + if not candidate: + continue + if _GIT_SHA_RE.fullmatch(candidate): + _commit_sha = candidate + logger.info("code revision stamping enabled from %s", source) + return + # An explicit argument or AGENT_COMMIT_SHA is a direct statement of + # intent, so a bad value there is worth surfacing. AGENT_VERSION is only + # a fallback and is expected to be a non-SHA tag much of the time, so + # falling through it quietly is correct, not a silent failure. + if source != _AGENT_VERSION_ENV: + logger.warning( + "%s=%r is not a git commit SHA; __commit_sha__ will not be stamped.", + source, + candidate, + ) + _commit_sha = None + return + + _commit_sha = None + logger.warning( + "code revision stamping was enabled but no commit SHA was found " + "(checked the commit_sha argument, %s, and %s); __commit_sha__ will not " + "be stamped. Set %s in the agent's environment -- e.g. bake it at build " + "time with a Dockerfile ARG/ENV.", + _COMMIT_SHA_ENV, + _AGENT_VERSION_ENV, + _COMMIT_SHA_ENV, + ) + + +def disable() -> None: + """Turn stamping back off (also used for test isolation).""" + global _commit_sha + _commit_sha = None + + +def is_enabled() -> bool: + """Whether a commit SHA resolved and will be stamped.""" + return _commit_sha is not None + + +def commit_sha() -> str | None: + """The resolved commit SHA, or ``None`` when stamping is not enabled.""" + return _commit_sha diff --git a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py index a1c0edca2..b42a4c13b 100644 --- a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py +++ b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py @@ -11,6 +11,7 @@ from scale_gp_beta.lib.tracing.span import Span as SGPSpan from agentex.types.span import Span +from agentex.lib.core.tracing import code_revision from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.lib.core.observability import tracing_metrics_recording as _metrics @@ -67,6 +68,11 @@ def _add_source_to_span(span: Span, env_vars: EnvironmentVariables) -> None: span.data["__agent_id__"] = env_vars.AGENT_ID if env_vars.AGENT_VERSION is not None: span.data["__agent_version__"] = env_vars.AGENT_VERSION + # Opt-in only (adk.code_revision.enable()); None unless the agent asked + # for it, so no agent inherits this by upgrading the SDK. + commit_sha = code_revision.commit_sha() + if commit_sha is not None: + span.data[code_revision.COMMIT_SHA_KEY] = commit_sha def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan: diff --git a/src/agentex/lib/environment_variables.py b/src/agentex/lib/environment_variables.py index 7d893e462..00dbbaada 100644 --- a/src/agentex/lib/environment_variables.py +++ b/src/agentex/lib/environment_variables.py @@ -25,6 +25,7 @@ class EnvVarKeys(str, Enum): AGENT_DESCRIPTION = "AGENT_DESCRIPTION" AGENT_ID = "AGENT_ID" AGENT_VERSION = "AGENT_VERSION" + AGENT_COMMIT_SHA = "AGENT_COMMIT_SHA" AGENT_API_KEY = "AGENT_API_KEY" # ACP Configuration ACP_URL = "ACP_URL" @@ -67,6 +68,12 @@ class EnvironmentVariables(BaseModel): AGENT_ID: str | None = None # Build/version discriminator (image tag or git sha), set by the deployment AGENT_VERSION: str | None = None + # The agent's source commit, baked into the image or set by the deployment. + # Unlike AGENT_VERSION this is expected to be a git SHA and nothing else, and + # it is OPT-IN: nothing is stamped unless the agent calls + # `adk.code_revision.enable()`, which also refuses a value that is not a git + # object name. See agentex.lib.core.tracing.code_revision. + AGENT_COMMIT_SHA: str | None = None AGENT_API_KEY: str | None = None ACP_TYPE: str | None = "async" AGENT_INPUT_TYPE: str | None = None diff --git a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py index 4a233fb72..c1403d237 100644 --- a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py +++ b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py @@ -54,6 +54,35 @@ def test_agent_identity_and_version_stamped_into_span_data(self): "__agent_version__": "sha-abc123", } + def test_commit_sha_is_not_stamped_without_opt_in(self, monkeypatch): + """Upgrading the SDK must not start emitting __commit_sha__ on its own, + even when the environment carries a perfectly good SHA.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span + + monkeypatch.setenv("AGENT_COMMIT_SHA", "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d") + code_revision.disable() + + env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) + span = _make_span() + _add_source_to_span(span, env) + assert "__commit_sha__" not in span.data + + def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch): + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span + + sha = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + monkeypatch.setenv("AGENT_COMMIT_SHA", sha) + code_revision.enable() + try: + env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) + span = _make_span() + _add_source_to_span(span, env) + assert span.data["__commit_sha__"] == sha + finally: + code_revision.disable() + def test_unset_identity_fields_are_omitted(self): from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span diff --git a/tests/lib/core/tracing/test_code_revision.py b/tests/lib/core/tracing/test_code_revision.py new file mode 100644 index 000000000..0b89b88f2 --- /dev/null +++ b/tests/lib/core/tracing/test_code_revision.py @@ -0,0 +1,109 @@ +"""Opt-in commit-SHA stamping. + +The contract that matters: an agent that does not call ``enable()`` gets nothing, +so upgrading the SDK never starts emitting this field on its own. +""" + +from __future__ import annotations + +import pytest + +from agentex.lib.core.tracing import code_revision + +SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + + +@pytest.fixture(autouse=True) +def _reset(): + """State is process-wide (like the lineage registry), so isolate each test.""" + code_revision.disable() + yield + code_revision.disable() + + +class TestOptIn: + def test_disabled_by_default(self, monkeypatch): + """Even with the env fully populated, nothing resolves until enable().""" + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + monkeypatch.setenv("AGENT_VERSION", SHA) + assert code_revision.commit_sha() is None + assert code_revision.is_enabled() is False + + def test_enable_reads_agent_commit_sha(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable() + assert code_revision.commit_sha() == SHA + assert code_revision.is_enabled() is True + + def test_explicit_argument_wins(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable("7f3a91c2") + assert code_revision.commit_sha() == "7f3a91c2" + + def test_disable_turns_it_back_off(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision.enable() + code_revision.disable() + assert code_revision.commit_sha() is None + + +class TestValueIsAlwaysACommit: + """A field named for a commit must never hold an image tag.""" + + @pytest.mark.parametrize( + "value", + [ + "latest", + "v1.2.3", + "0.2.4-v4", + "rocket_mock_agent-b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d", # AWS ECR composite + "abc", # shorter than git's 7-char minimum + "z" * 40, # right length, not hex + ], + ) + def test_non_sha_is_refused(self, monkeypatch, value): + monkeypatch.setenv("AGENT_COMMIT_SHA", value) + code_revision.enable() + assert code_revision.commit_sha() is None + + @pytest.mark.parametrize("value", [SHA, SHA.upper(), "b362b17", "a" * 64]) + def test_git_object_names_are_accepted(self, monkeypatch, value): + monkeypatch.setenv("AGENT_COMMIT_SHA", value) + code_revision.enable() + assert code_revision.commit_sha() == value + + def test_whitespace_only_is_refused(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", " ") + code_revision.enable() + assert code_revision.commit_sha() is None + + def test_enable_with_nothing_available_is_a_no_op(self, monkeypatch): + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.delenv("AGENT_VERSION", raising=False) + code_revision.enable() + assert code_revision.commit_sha() is None + + +class TestAgentVersionFallback: + def test_falls_back_to_agent_version_when_sha_shaped(self, monkeypatch): + """A platform deploy already sets AGENT_VERSION; on GCP/Azure it is a + bare SHA, so an opting-in agent needs no extra plumbing.""" + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.setenv("AGENT_VERSION", SHA) + code_revision.enable() + assert code_revision.commit_sha() == SHA + + def test_does_not_fall_back_to_a_non_sha_agent_version(self, monkeypatch): + """AGENT_VERSION is 'latest' or an AWS composite much of the time.""" + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) + monkeypatch.setenv("AGENT_VERSION", "latest") + code_revision.enable() + assert code_revision.commit_sha() is None + + def test_bad_explicit_value_does_not_fall_through(self, monkeypatch): + """An explicit AGENT_COMMIT_SHA is a statement of intent: if it is wrong, + say so rather than silently substituting the image tag.""" + monkeypatch.setenv("AGENT_COMMIT_SHA", "not-a-sha") + monkeypatch.setenv("AGENT_VERSION", SHA) + code_revision.enable() + assert code_revision.commit_sha() is None From 4d860fcb4d1b249fa68dc636c93f12179e53d2bc Mon Sep 17 00:00:00 2001 From: Cynthia Wang Date: Fri, 28 Aug 2026 14:11:24 -0400 Subject: [PATCH 2/3] fix(tests): narrow span.data before subscripting in commit-SHA tests `Span.data` is `dict | list[dict] | None`, so `in` and `[]` need an isinstance guard to satisfy the project-wide pyright run in scripts/lint. Co-Authored-By: Claude Opus 5 --- tests/lib/core/tracing/processors/test_sgp_tracing_processor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py index c1403d237..54d532528 100644 --- a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py +++ b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py @@ -66,6 +66,7 @@ def test_commit_sha_is_not_stamped_without_opt_in(self, monkeypatch): env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) span = _make_span() _add_source_to_span(span, env) + assert isinstance(span.data, dict) assert "__commit_sha__" not in span.data def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch): @@ -79,6 +80,7 @@ def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch): env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) span = _make_span() _add_source_to_span(span, env) + assert isinstance(span.data, dict) assert span.data["__commit_sha__"] == sha finally: code_revision.disable() From 45747c823dce1fc6d847e2b6b18b0b21cea21334 Mon Sep 17 00:00:00 2001 From: Cynthia Wang Date: Fri, 28 Aug 2026 14:45:14 -0400 Subject: [PATCH 3/3] fix(tracing): keep the commit SHA out of the shared business span trace.py hands ONE Span instance to every registered processor, and _add_source_to_span mutates span.data in place. So writing __commit_sha__ there leaked it: a co-registered Agentex processor serialized it too, and it surfaced in caller-visible span.data -- contradicting the claim that this field is SGP-scoped. Build the SGP write's metadata as a copy instead. Adds a regression test that asserts the SGP metadata carries the key while the shared span and the Agentex processor's payload do not, plus one for list-shaped data, which has nowhere to put a metadata key and is now returned untouched. The __source__ / __agent_* keys leak the same way today; left alone deliberately, since changing five long-shipped fields is out of scope here. Reported in review by Greptile. Co-Authored-By: Claude Opus 5 --- .../processors/sgp_tracing_processor.py | 32 +++++++--- .../processors/test_sgp_tracing_processor.py | 59 ++++++++++++++----- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py index b42a4c13b..9ee269231 100644 --- a/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py +++ b/src/agentex/lib/core/tracing/processors/sgp_tracing_processor.py @@ -3,7 +3,7 @@ import os import asyncio import weakref -from typing import cast, override +from typing import Any, cast, override import scale_gp_beta.lib.tracing as tracing from scale_gp_beta import SGPClient, AsyncSGPClient @@ -68,11 +68,29 @@ def _add_source_to_span(span: Span, env_vars: EnvironmentVariables) -> None: span.data["__agent_id__"] = env_vars.AGENT_ID if env_vars.AGENT_VERSION is not None: span.data["__agent_version__"] = env_vars.AGENT_VERSION - # Opt-in only (adk.code_revision.enable()); None unless the agent asked - # for it, so no agent inherits this by upgrading the SDK. - commit_sha = code_revision.commit_sha() - if commit_sha is not None: - span.data[code_revision.COMMIT_SHA_KEY] = commit_sha + + +def _sgp_metadata(span: Span) -> Any: + """Metadata for the SGP write: ``span.data`` plus the opt-in commit SHA. + + Returns a COPY rather than mutating ``span``. ``trace.py`` hands the same + Span instance to every registered processor, so anything written onto + ``span.data`` here would also be serialized by the Agentex processor and + show up in caller-visible span data. ``__commit_sha__`` is opt-in and + SGP-scoped, so it must not leak that way. + + (The ``__source__`` / ``__agent_*`` keys set by ``_add_source_to_span`` do + leak like that today. Left as-is: changing five long-shipped fields is not + this change's business.) + """ + commit_sha = code_revision.commit_sha() + if commit_sha is None: + return span.data + if isinstance(span.data, dict): + return {**span.data, code_revision.COMMIT_SHA_KEY: commit_sha} + # List-shaped data is an accepted `data` shape and has nowhere to put a + # metadata key; leave it untouched rather than dropping the caller's data. + return span.data def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan: @@ -88,7 +106,7 @@ def _build_sgp_span(span: Span, env_vars: EnvironmentVariables) -> SGPSpan: trace_id=span.trace_id, input=span.input, output=span.output, - metadata=span.data, + metadata=_sgp_metadata(span), ), ) sgp_span.start_time = span.start_time.isoformat() # type: ignore[union-attr] diff --git a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py index 54d532528..6cd324f01 100644 --- a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py +++ b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py @@ -54,34 +54,63 @@ def test_agent_identity_and_version_stamped_into_span_data(self): "__agent_version__": "sha-abc123", } + SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + def test_commit_sha_is_not_stamped_without_opt_in(self, monkeypatch): """Upgrading the SDK must not start emitting __commit_sha__ on its own, even when the environment carries a perfectly good SHA.""" from agentex.lib.core.tracing import code_revision - from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata - monkeypatch.setenv("AGENT_COMMIT_SHA", "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d") + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) code_revision.disable() - env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) - span = _make_span() - _add_source_to_span(span, env) - assert isinstance(span.data, dict) - assert "__commit_sha__" not in span.data + span = _make_span(); span.data = {} + assert "__commit_sha__" not in (_sgp_metadata(span) or {}) def test_commit_sha_is_stamped_after_opt_in(self, monkeypatch): from agentex.lib.core.tracing import code_revision - from agentex.lib.core.tracing.processors.sgp_tracing_processor import _add_source_to_span + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata - sha = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" - monkeypatch.setenv("AGENT_COMMIT_SHA", sha) + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) code_revision.enable() try: - env = MagicMock(ACP_TYPE=None, AGENT_NAME=None, AGENT_ID=None, AGENT_VERSION=None) - span = _make_span() - _add_source_to_span(span, env) - assert isinstance(span.data, dict) - assert span.data["__commit_sha__"] == sha + span = _make_span(); span.data = {"caller": "kept"} + metadata = _sgp_metadata(span) + assert metadata["__commit_sha__"] == self.SHA + assert metadata["caller"] == "kept" + finally: + code_revision.disable() + + def test_commit_sha_does_not_leak_onto_the_shared_span(self, monkeypatch): + """trace.py hands ONE Span to every processor. If the commit SHA were + written onto span.data, a co-registered Agentex processor would + serialize it too, and it would surface in caller-visible span data.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + from agentex.lib.core.tracing.processors.agentex_tracing_processor import _create_kwargs + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.enable() + try: + span = _make_span(); span.data = {} + assert _sgp_metadata(span)["__commit_sha__"] == self.SHA # SGP sees it + assert "__commit_sha__" not in span.data # the span does not + assert "__commit_sha__" not in (_create_kwargs(span)["data"] or {}) + finally: + code_revision.disable() + + def test_list_shaped_data_is_left_alone(self, monkeypatch): + """`data` may be a list of dicts; there is nowhere to put a metadata key, + and dropping the caller's data would be worse than omitting the field.""" + from agentex.lib.core.tracing import code_revision + from agentex.lib.core.tracing.processors.sgp_tracing_processor import _sgp_metadata + + monkeypatch.setenv("AGENT_COMMIT_SHA", self.SHA) + code_revision.enable() + try: + span = _make_span(); span.data = [{"a": 1}] + assert _sgp_metadata(span) == [{"a": 1}] finally: code_revision.disable()