diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md new file mode 100644 index 000000000..2d9bac717 --- /dev/null +++ b/NEXT_CHANGELOG.md @@ -0,0 +1,19 @@ +# NEXT CHANGELOG + +## Release v0.118.0 + +### New Features and Improvements + +* Added a `meta-harness` user-agent dimension that reports the omnigent meta-harness (detected via the `OMNIGENT` environment variable) independently of agent detection. + +### Security + +### Bug Fixes + +### Documentation + +### Breaking Changes + +### Internal Changes + +### API Changes diff --git a/databricks/sdk/useragent.py b/databricks/sdk/useragent.py index af0d054a7..90bdb55f4 100644 --- a/databricks/sdk/useragent.py +++ b/databricks/sdk/useragent.py @@ -12,6 +12,7 @@ RUNTIME_KEY = "runtime" CICD_KEY = "cicd" AUTH_KEY = "auth" +META_HARNESS_KEY = "meta-harness" _product_name = "unknown" _product_version = "0.0.0" @@ -175,6 +176,9 @@ def to_string( agent = agent_provider() if agent: base.append(("agent", agent)) + meta_harness = meta_harness_provider() + if meta_harness: + base.append((META_HARNESS_KEY, meta_harness)) return " ".join(f"{k}/{v}" for k, v in base) @@ -329,3 +333,37 @@ def _agent_env_fallback() -> str: if not v: return "" return _sanitize_agent_value(v)[:_MAX_AGENT_FALLBACK_LEN] + + +@dataclass(frozen=True) +class _MetaHarnessRecord: + env_var: str + product: str + + +# Known agent meta-harnesses, detected independently of agents (a meta-harness +# is not an agent). Keep in sync with databricks-sdk-go and databricks-sdk-java. +_KNOWN_META_HARNESSES: List[_MetaHarnessRecord] = [ + _MetaHarnessRecord("OMNIGENT", "omnigent"), # https://github.com/omnigent-ai/omnigent +] + +# None = not computed, "" = computed but no meta-harness found. +_meta_harness_provider = None + + +def meta_harness_provider() -> str: + """Detect a known agent meta-harness by presence-only env var, else "". + + Returns "multiple" if more than one matched. Cached after the first call. + """ + global _meta_harness_provider + if _meta_harness_provider is not None: + return _meta_harness_provider + matches = [h.product for h in _KNOWN_META_HARNESSES if h.env_var in os.environ] + if len(matches) == 1: + _meta_harness_provider = matches[0] + elif len(matches) > 1: + _meta_harness_provider = "multiple" + else: + _meta_harness_provider = "" + return _meta_harness_provider diff --git a/tests/test_config.py b/tests/test_config.py index e13aaa46a..67d14a975 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -82,6 +82,7 @@ def system(self): monkeypatch.setattr(useragent, "_extra", []) monkeypatch.setattr(useragent, "_cicd_provider", None) monkeypatch.setattr(useragent, "_agent_provider", None) + monkeypatch.setattr(useragent, "_meta_harness_provider", None) monkeypatch.setattr(platform, "python_version", lambda: "3.0.0") monkeypatch.setattr(platform, "uname", MockUname) diff --git a/tests/test_core.py b/tests/test_core.py index 364c830a9..4c1b3a699 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -275,6 +275,7 @@ def system(self): monkeypatch.setattr(useragent, "_extra", []) monkeypatch.setattr(useragent, "_cicd_provider", None) monkeypatch.setattr(useragent, "_agent_provider", None) + monkeypatch.setattr(useragent, "_meta_harness_provider", None) monkeypatch.setattr(platform, "python_version", lambda: "3.0.0") monkeypatch.setattr(platform, "uname", MockUname) diff --git a/tests/test_user_agent.py b/tests/test_user_agent.py index 3c542e752..2fb78ae44 100644 --- a/tests/test_user_agent.py +++ b/tests/test_user_agent.py @@ -68,11 +68,12 @@ def clean_useragent_env(): original_env = os.environ.copy() os.environ.clear() - # Clear cached CICD and agent providers. + # Clear cached CICD, agent, and meta-harness providers. from databricks.sdk import useragent useragent._cicd_provider = None useragent._agent_provider = None + useragent._meta_harness_provider = None yield @@ -81,6 +82,7 @@ def clean_useragent_env(): os.environ.update(original_env) useragent._cicd_provider = None useragent._agent_provider = None + useragent._meta_harness_provider = None def test_user_agent_cicd_no_provider(clean_useragent_env): @@ -493,3 +495,64 @@ def test_agent_provider_cached(clean_useragent_env): os.environ["CLAUDECODE"] = "1" assert useragent.agent_provider() == "cursor" + + +def test_meta_harness_provider_no_meta_harness(clean_useragent_env): + from databricks.sdk import useragent + + assert useragent.meta_harness_provider() == "" + + +def test_meta_harness_provider_omnigent(clean_useragent_env): + os.environ["OMNIGENT"] = "1" + from databricks.sdk import useragent + + assert useragent.meta_harness_provider() == "omnigent" + + +def test_meta_harness_provider_omnigent_empty_value_still_counts_as_set(clean_useragent_env): + # Presence-only matcher: an empty value still fires. + os.environ["OMNIGENT"] = "" + from databricks.sdk import useragent + + assert useragent.meta_harness_provider() == "omnigent" + + +def test_meta_harness_provider_cached(clean_useragent_env): + os.environ["OMNIGENT"] = "1" + from databricks.sdk import useragent + + assert useragent.meta_harness_provider() == "omnigent" + + # Change the environment: the cached result should persist. + del os.environ["OMNIGENT"] + + assert useragent.meta_harness_provider() == "omnigent" + + +def test_user_agent_string_includes_meta_harness(clean_useragent_env): + os.environ["OMNIGENT"] = "1" + from databricks.sdk import useragent + + ua = useragent.to_string() + assert "meta-harness/omnigent" in ua + + +def test_user_agent_string_no_meta_harness(clean_useragent_env): + from databricks.sdk import useragent + + ua = useragent.to_string() + assert "meta-harness/" not in ua + + +def test_meta_harness_independent_of_agent(clean_useragent_env): + # omnigent spawns the real agent CLI, so both env vars are set: the UA must + # carry both dimensions and omnigent must not trip the agent "multiple" signal. + os.environ["CLAUDECODE"] = "1" + os.environ["OMNIGENT"] = "1" + from databricks.sdk import useragent + + ua = useragent.to_string() + assert "agent/claude-code" in ua + assert "meta-harness/omnigent" in ua + assert useragent.agent_provider() == "claude-code"