diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 95c44cfb4..00f7ba59f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,4 @@ { - ".": "0.26.0", - "adk": "0.26.0" + ".": "0.27.0", + "adk": "0.27.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ffe61e7..a3c4ccd62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,21 @@ * **tracing:** emit OTel metrics for async span queue depth, batch drain, and SGP export success/failure (HTTP status labels). Disable SDK-side recording with ``AGENTEX_TRACING_METRICS=0``. +## 0.27.0 (2026-09-16) + +Full Changelog: [agentex-client-v0.26.0...agentex-client-v0.27.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.26.0...agentex-client-v0.27.0) + +### Features + +* **obs:** wire sgp-obs from the SDK for traces, metrics and logs ([#518](https://github.com/scaleapi/scale-agentex-python/issues/518)) ([d88fac6](https://github.com/scaleapi/scale-agentex-python/commit/d88fac6a0b3e572a6346106a615265b2354d1f79)) +* **registration:** report the agent's commit and source repo at registration ([#508](https://github.com/scaleapi/scale-agentex-python/issues/508)) ([94335d7](https://github.com/scaleapi/scale-agentex-python/commit/94335d71ece7bfff3ba684fe43991e4bd2397295)) +* **tracing:** stamp __commit_sha__ automatically when AGENT_COMMIT_SHA is set ([#507](https://github.com/scaleapi/scale-agentex-python/issues/507)) ([53ab900](https://github.com/scaleapi/scale-agentex-python/commit/53ab9007ab2a78528c38fe254929b92ccce740a3)) + + +### Reverts + +* **obs:** remove sgp-obs beta changes ([#522](https://github.com/scaleapi/scale-agentex-python/issues/522)) ([687ebfb](https://github.com/scaleapi/scale-agentex-python/commit/687ebfbf874e1feb01e102c7130cebb7f26e387a)) + ## 0.26.0 (2026-09-14) Full Changelog: [agentex-client-v0.25.0...agentex-client-v0.26.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.25.0...agentex-client-v0.26.0) diff --git a/adk/CHANGELOG.md b/adk/CHANGELOG.md index 3b2cc8d62..57a59d3d4 100644 --- a/adk/CHANGELOG.md +++ b/adk/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.27.0 (2026-09-16) + +Full Changelog: [agentex-sdk-v0.26.0...agentex-sdk-v0.27.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.26.0...agentex-sdk-v0.27.0) + +### Features + +* **obs:** wire sgp-obs from the SDK for traces, metrics and logs ([#518](https://github.com/scaleapi/scale-agentex-python/issues/518)) ([d88fac6](https://github.com/scaleapi/scale-agentex-python/commit/d88fac6a0b3e572a6346106a615265b2354d1f79)) + + +### Reverts + +* **obs:** remove sgp-obs beta changes ([#522](https://github.com/scaleapi/scale-agentex-python/issues/522)) ([687ebfb](https://github.com/scaleapi/scale-agentex-python/commit/687ebfbf874e1feb01e102c7130cebb7f26e387a)) + ## 0.26.0 (2026-09-14) Full Changelog: [agentex-sdk-v0.25.0...agentex-sdk-v0.26.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.25.0...agentex-sdk-v0.26.0) diff --git a/adk/pyproject.toml b/adk/pyproject.toml index b42b50e11..146f744a5 100644 --- a/adk/pyproject.toml +++ b/adk/pyproject.toml @@ -4,7 +4,7 @@ # (agentex/{__init__.py, _*.py, types/, resources/}) ships from the slim # sibling package `agentex-client` which is pinned as a runtime dep. name = "agentex-sdk" -version = "0.26.0" +version = "0.27.0" description = "Agent Development Kit (ADK) overlay for the Agentex API — FastACP server, Temporal workflows, LLM provider integrations, observability" license = "Apache-2.0" authors = [ diff --git a/pyproject.toml b/pyproject.toml index 8fb1f97cf..7576c1272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ # overlay (formerly `src/agentex/lib/*`) now lives in `adk/` and ships # as the sibling `agentex-sdk` package — see `adk/pyproject.toml`. name = "agentex-client" -version = "0.26.0" +version = "0.27.0" description = "The official Python REST client for the Agentex API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/agentex/_version.py b/src/agentex/_version.py index 34aa48bd8..d40596c81 100644 --- a/src/agentex/_version.py +++ b/src/agentex/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "agentex" -__version__ = "0.26.0" # x-release-please-version +__version__ = "0.27.0" # x-release-please-version diff --git a/src/agentex/lib/core/tracing/code_revision.py b/src/agentex/lib/core/tracing/code_revision.py index 7b08dd45f..570d4f1cd 100644 --- a/src/agentex/lib/core/tracing/code_revision.py +++ b/src/agentex/lib/core/tracing/code_revision.py @@ -1,10 +1,11 @@ -"""Opt-in stamping of the agent's source commit onto its spans. +"""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__:``. +Stamping turns on when the process starts with ``AGENT_COMMIT_SHA`` set, which +the SGP cloud deploy does from the build record's attested commit, or when the +agent calls :func:`enable` itself. Nothing is stamped otherwise: upgrading the +SDK alone never starts emitting the field. When on, 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 @@ -21,7 +22,7 @@ from agentex.lib.utils.logging import make_logger -__all__ = ("COMMIT_SHA_KEY", "enable", "disable", "is_enabled", "commit_sha") +__all__ = ("COMMIT_SHA_KEY", "enable", "disable", "is_enabled", "commit_sha", "is_git_object_name") logger = make_logger(__name__) @@ -31,6 +32,12 @@ # git's own 7-character minimum. _GIT_SHA_RE = re.compile(r"[0-9a-fA-F]{7,64}") + +def is_git_object_name(value: str) -> bool: + """Whether ``value`` is a full or abbreviated git SHA-1/SHA-256 object name.""" + return _GIT_SHA_RE.fullmatch(value.strip()) is not None + + _COMMIT_SHA_ENV = "AGENT_COMMIT_SHA" # Fallback only: automatic, and only usable when it happens to be SHA-shaped. _AGENT_VERSION_ENV = "AGENT_VERSION" @@ -42,13 +49,16 @@ def enable(commit_sha: str | None = None) -> None: - """Opt this process in to stamping ``__commit_sha__`` onto every span. + """Turn on stamping ``__commit_sha__`` onto every span from this process. 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. + + Called once at import when ``AGENT_COMMIT_SHA`` is set, so a deployment that + supplies the commit needs no code change in the agent. """ global _commit_sha @@ -103,3 +113,12 @@ def is_enabled() -> bool: def commit_sha() -> str | None: """The resolved commit SHA, or ``None`` when stamping is not enabled.""" return _commit_sha + + +def _enable_from_environment() -> None: + """Auto-enable on ``AGENT_COMMIT_SHA`` only; ``AGENT_VERSION`` stays an explicit fallback.""" + if os.environ.get(_COMMIT_SHA_ENV, "").strip(): + enable() + + +_enable_from_environment() diff --git a/src/agentex/lib/environment_variables.py b/src/agentex/lib/environment_variables.py index 00dbbaada..dae1e5db3 100644 --- a/src/agentex/lib/environment_variables.py +++ b/src/agentex/lib/environment_variables.py @@ -26,6 +26,7 @@ class EnvVarKeys(str, Enum): AGENT_ID = "AGENT_ID" AGENT_VERSION = "AGENT_VERSION" AGENT_COMMIT_SHA = "AGENT_COMMIT_SHA" + AGENT_SOURCE_REPO = "AGENT_SOURCE_REPO" AGENT_API_KEY = "AGENT_API_KEY" # ACP Configuration ACP_URL = "ACP_URL" @@ -68,12 +69,11 @@ 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. + # The agent's source commit, set by the deployment or baked into the image; a git + # SHA and nothing else. Stamped as __commit_sha__ when set (see tracing.code_revision). AGENT_COMMIT_SHA: str | None = None + # Git remote the agent was built from (any URL form; normalized to host/path on use). + AGENT_SOURCE_REPO: str | None = None AGENT_API_KEY: str | None = None ACP_TYPE: str | None = "async" AGENT_INPUT_TYPE: str | None = None diff --git a/src/agentex/lib/utils/build_provenance.py b/src/agentex/lib/utils/build_provenance.py index 447980263..37b61a3f9 100644 --- a/src/agentex/lib/utils/build_provenance.py +++ b/src/agentex/lib/utils/build_provenance.py @@ -82,7 +82,8 @@ def normalize_remote(url: Optional[str]) -> Optional[str]: """Strip credentials and scheme from a remote, returning ``host/path``.""" if not url: return None - candidate = url.strip() + # Query strings and fragments never name a repo, but they do carry tokens. + candidate = url.strip().split("?", 1)[0].split("#", 1)[0] # scp-like syntax: git@host:org/repo(.git) — no scheme, host/path split on ':' if "://" not in candidate and ":" in candidate and "/" not in candidate.split(":", 1)[0]: candidate = candidate.split("@", 1)[-1].replace(":", "/", 1) diff --git a/src/agentex/lib/utils/registration.py b/src/agentex/lib/utils/registration.py index 5fc4d4be5..36b5f9a04 100644 --- a/src/agentex/lib/utils/registration.py +++ b/src/agentex/lib/utils/registration.py @@ -7,6 +7,8 @@ from agentex.lib.utils.logging import make_logger from agentex.lib.environment_variables import EnvironmentVariables +from agentex.lib.utils.build_provenance import normalize_remote +from agentex.lib.core.tracing.code_revision import is_git_object_name logger = make_logger(__name__) @@ -20,6 +22,29 @@ def get_auth_principal(env_vars: EnvironmentVariables): except Exception: return None + +def build_registration_metadata(env_vars: EnvironmentVariables, agent_card=None) -> dict: + """Deployment id, source provenance, and agent card; keys appear only when known.""" + metadata: dict = {} + if env_vars.AGENTEX_DEPLOYMENT_ID: + metadata["deployment_id"] = env_vars.AGENTEX_DEPLOYMENT_ID + commit = (env_vars.AGENT_COMMIT_SHA or "").strip() + if commit: + if is_git_object_name(commit): + metadata["commit_sha"] = commit + else: + logger.warning( + "AGENT_COMMIT_SHA=%r is not a git commit SHA; commit_sha omitted from registration.", + commit, + ) + repo = normalize_remote(env_vars.AGENT_SOURCE_REPO) + if repo: + metadata["source_repo"] = repo + if agent_card is not None: + metadata["agent_card"] = agent_card.model_dump() if hasattr(agent_card, "model_dump") else agent_card + return metadata + + async def register_agent(env_vars: EnvironmentVariables, agent_card=None): """Register this agent with the Agentex server""" if not env_vars.AGENTEX_BASE_URL: @@ -33,13 +58,7 @@ async def register_agent(env_vars: EnvironmentVariables, agent_card=None): or f"Generic description for agent: {env_vars.AGENT_NAME}" ) - # Registration metadata carries the deployment id and agent card. - registration_metadata: dict = {} - if env_vars.AGENTEX_DEPLOYMENT_ID: - registration_metadata["deployment_id"] = env_vars.AGENTEX_DEPLOYMENT_ID - if agent_card is not None: - card_data = agent_card.model_dump() if hasattr(agent_card, "model_dump") else agent_card - registration_metadata["agent_card"] = card_data + registration_metadata = build_registration_metadata(env_vars, agent_card) # Prepare registration data registration_data = { 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 6cd324f01..7b5c129d6 100644 --- a/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py +++ b/tests/lib/core/tracing/processors/test_sgp_tracing_processor.py @@ -56,13 +56,13 @@ def test_agent_identity_and_version_stamped_into_span_data(self): 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.""" + def test_commit_sha_is_not_stamped_when_env_absent(self, monkeypatch): + """Upgrading the SDK must not start emitting __commit_sha__ on its own; + only AGENT_COMMIT_SHA or an enable() call turns it on.""" 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) + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) code_revision.disable() span = _make_span(); span.data = {} diff --git a/tests/lib/core/tracing/test_code_revision.py b/tests/lib/core/tracing/test_code_revision.py index 0b89b88f2..a696129d7 100644 --- a/tests/lib/core/tracing/test_code_revision.py +++ b/tests/lib/core/tracing/test_code_revision.py @@ -1,7 +1,8 @@ -"""Opt-in commit-SHA stamping. +"""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. +The contract that matters: with ``AGENT_COMMIT_SHA`` absent and no ``enable()`` +call, nothing is stamped, so upgrading the SDK never starts emitting this field +on its own. A deployment that sets the env var turns it on without agent code. """ from __future__ import annotations @@ -21,14 +22,33 @@ def _reset(): 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) +class TestEnablement: + def test_off_when_env_absent(self, monkeypatch): + """The import-time hook ignores AGENT_VERSION; that fallback needs enable().""" + monkeypatch.delenv("AGENT_COMMIT_SHA", raising=False) monkeypatch.setenv("AGENT_VERSION", SHA) + code_revision._enable_from_environment() assert code_revision.commit_sha() is None assert code_revision.is_enabled() is False + def test_env_set_at_startup_enables_without_a_call(self, monkeypatch): + """The cloud deploy sets AGENT_COMMIT_SHA from the build record; the agent + should not need to know.""" + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + code_revision._enable_from_environment() + assert code_revision.commit_sha() == SHA + + def test_env_set_after_import_needs_enable(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) + assert code_revision.commit_sha() is None + code_revision.enable() + assert code_revision.commit_sha() == SHA + + def test_bad_env_at_startup_leaves_it_off(self, monkeypatch): + monkeypatch.setenv("AGENT_COMMIT_SHA", "latest") + code_revision._enable_from_environment() + assert code_revision.commit_sha() is None + def test_enable_reads_agent_commit_sha(self, monkeypatch): monkeypatch.setenv("AGENT_COMMIT_SHA", SHA) code_revision.enable() diff --git a/tests/lib/test_agent_card.py b/tests/lib/test_agent_card.py index f9a99ffc5..7246d7c32 100644 --- a/tests/lib/test_agent_card.py +++ b/tests/lib/test_agent_card.py @@ -377,6 +377,8 @@ def mock_env_vars(self): "AGENT_ID": None, "AGENT_INPUT_TYPE": None, "AGENT_API_KEY": None, + "AGENT_COMMIT_SHA": None, + "AGENT_SOURCE_REPO": None, "AGENTEX_DEPLOYMENT_ID": None, })() return mock diff --git a/tests/lib/test_agentex_worker.py b/tests/lib/test_agentex_worker.py index 742ac3e74..b0bf47a63 100644 --- a/tests/lib/test_agentex_worker.py +++ b/tests/lib/test_agentex_worker.py @@ -140,6 +140,8 @@ def _env_vars_mock(): env.AGENTEX_DEPLOYMENT_ID = None env.AGENT_ID = None env.AGENT_INPUT_TYPE = None + env.AGENT_COMMIT_SHA = None + env.AGENT_SOURCE_REPO = None return env @staticmethod diff --git a/tests/lib/test_build_provenance.py b/tests/lib/test_build_provenance.py index 9115e2804..1bf3629d0 100644 --- a/tests/lib/test_build_provenance.py +++ b/tests/lib/test_build_provenance.py @@ -50,6 +50,7 @@ def _write(root: Path, rel: str, content: str = "x") -> None: ("https://github.com/scaleapi/Repo.git", "github.com/scaleapi/Repo"), ("https://x-token:secret@GitHub.com/scaleapi/Repo", "github.com/scaleapi/Repo"), ("ssh://git@gitlab.com/group/sub/proj.git", "gitlab.com/group/sub/proj"), + ("https://github.com/scaleapi/Repo.git?access_token=SECRET#frag", "github.com/scaleapi/Repo"), ("", None), (None, None), ], diff --git a/tests/lib/utils/test_registration.py b/tests/lib/utils/test_registration.py new file mode 100644 index 000000000..65960d757 --- /dev/null +++ b/tests/lib/utils/test_registration.py @@ -0,0 +1,49 @@ +"""Registration metadata: what an agent reports about itself at startup.""" + +from __future__ import annotations + +import pytest + +from agentex.lib.utils.registration import build_registration_metadata +from agentex.lib.environment_variables import EnvironmentVariables + +SHA = "b362b171a9c4e1f09d8e7a6b5c4d3e2f1a0b9c8d" + + +def _env(**overrides) -> EnvironmentVariables: + return EnvironmentVariables(AGENT_NAME="sample-agent", ACP_URL="http://agent", **overrides) + + +def test_nothing_known_yields_empty_metadata(): + assert build_registration_metadata(_env()) == {} + + +def test_commit_and_repo_reported_when_set(): + env = _env(AGENT_COMMIT_SHA=SHA, AGENT_SOURCE_REPO="git@github.com:scaleapi/Demo.git") + assert build_registration_metadata(env) == { + "commit_sha": SHA, + "source_repo": "github.com/scaleapi/Demo", + } + + +@pytest.mark.parametrize("value", ["latest", "v1.2.3", "rocket_mock_agent-" + SHA, "abc", " "]) +def test_non_commit_values_are_omitted_not_forwarded(value): + """A field named for a commit never holds an image tag, same rule as __commit_sha__.""" + assert "commit_sha" not in build_registration_metadata(_env(AGENT_COMMIT_SHA=value)) + + +def test_repo_normalization_strips_scheme_and_credentials(): + env = _env(AGENT_SOURCE_REPO="https://x-token:secret@GitHub.com/scaleapi/Demo.git") + assert build_registration_metadata(env)["source_repo"] == "github.com/scaleapi/Demo" + + +def test_deployment_id_and_agent_card_still_reported(): + class Card: + def model_dump(self): + return {"name": "sample"} + + env = _env(AGENTEX_DEPLOYMENT_ID="dep-1") + assert build_registration_metadata(env, Card()) == { + "deployment_id": "dep-1", + "agent_card": {"name": "sample"}, + }