diff --git a/src/agentex/lib/core/tracing/code_revision.py b/src/agentex/lib/core/tracing/code_revision.py index 7b08dd45f..beaa7a521 100644 --- a/src/agentex/lib/core/tracing/code_revision.py +++ b/src/agentex/lib/core/tracing/code_revision.py @@ -21,7 +21,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 +31,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" diff --git a/src/agentex/lib/environment_variables.py b/src/agentex/lib/environment_variables.py index 00dbbaada..317f18ace 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" @@ -74,6 +75,8 @@ class EnvironmentVariables(BaseModel): # `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 + # 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/test_agent_card.py b/tests/lib/test_agent_card.py index 5d57f9e8e..42ccc4442 100644 --- a/tests/lib/test_agent_card.py +++ b/tests/lib/test_agent_card.py @@ -333,6 +333,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_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"}, + }