From 96e40ed3a427a94776c6336bf03c15c61348ad4e Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Fri, 7 Aug 2026 15:22:36 -0700 Subject: [PATCH] fix(telemetry): detect runtime markers by presence, split paas from serverless Three findings from the CodeRabbit, code-quality and Cursor reviews. The runtime loop tested truthiness while constants.py documented presence, so a platform exporting a bare CI= fell through to the TTY fallback and was mislabelled as an ordinary local run. Presence is now what it says. The assistant markers keep truthiness deliberately: there an empty value means the tool set a placeholder rather than claiming the session. DYNO and WEBSITE_INSTANCE_ID marked Heroku dynos and Azure App Service instances as serverless, and since serverless is checked first they could never reach the container label. They move to a paas context, which is what they are: long-lived containers rather than per-invocation functions. AWS_EXECUTION_ENV is dropped entirely - it is set on ECS and EC2 as well as Lambda, and AWS_LAMBDA_FUNCTION_NAME already covers Lambda without the collision. The container probe no longer wraps os.path.exists in a try/except. os.path.exists handles OSError internally and returns False, so the handler guarded a condition that cannot occur and only hid the intent. Co-Authored-By: Claude Opus 5 (1M context) --- lib/crewai/src/crewai/telemetry/utils.py | 12 +++---- lib/crewai/src/crewai/utilities/constants.py | 11 +++++-- .../telemetry/test_coding_agent_detection.py | 33 +++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/lib/crewai/src/crewai/telemetry/utils.py b/lib/crewai/src/crewai/telemetry/utils.py index 32805a55c..823afc5dc 100644 --- a/lib/crewai/src/crewai/telemetry/utils.py +++ b/lib/crewai/src/crewai/telemetry/utils.py @@ -105,19 +105,19 @@ def detect_runtime_context() -> str: "unknown" when that check cannot be made. The result is always a member of KNOWN_RUNTIME_CONTEXTS. """ + # Presence, not truthiness: a platform that exports an empty CI= is still + # CI, unlike the assistant markers where an empty value means the tool set + # a placeholder rather than claiming the session. for context_name, env_vars in RUNTIME_CONTEXT_ENV_MARKERS: - if any(os.environ.get(env_var) for env_var in env_vars): + if any(env_var in os.environ for env_var in env_vars): return context_name for env_var, expected, context_name in _EDITOR_TERM_MARKERS: if os.environ.get(env_var) == expected: return context_name - try: - if os.path.exists(_DOCKER_ENV_PATH): - return "container" - except OSError: - pass + if os.path.exists(_DOCKER_ENV_PATH): + return "container" try: return "interactive" if sys.stdout.isatty() else "non_interactive" diff --git a/lib/crewai/src/crewai/utilities/constants.py b/lib/crewai/src/crewai/utilities/constants.py index 9a1c6551a..b22f98b7f 100644 --- a/lib/crewai/src/crewai/utilities/constants.py +++ b/lib/crewai/src/crewai/utilities/constants.py @@ -25,6 +25,7 @@ __all__ = [ "MAX_FILE_NAME_LENGTH", "NOTEBOOK_ENV_VARS", "NOT_SPECIFIED", + "PAAS_ENV_VARS", "RUNTIME_CONTEXT_ENV_MARKERS", "SERVERLESS_ENV_VARS", "TRAINED_AGENTS_DATA_FILE", @@ -98,12 +99,17 @@ CI_ENV_VARS: Final[tuple[str, ...]] = ( "TRAVIS", ) SERVERLESS_ENV_VARS: Final[tuple[str, ...]] = ( - "AWS_EXECUTION_ENV", "AWS_LAMBDA_FUNCTION_NAME", - "DYNO", "FUNCTION_TARGET", "K_SERVICE", "VERCEL", +) +# Managed application platforms, kept apart from serverless: their markers are +# set for long-lived containers rather than per-invocation functions, and +# checking them under "serverless" would have claimed every Heroku dyno and +# Azure App Service instance before the container check could see them. +PAAS_ENV_VARS: Final[tuple[str, ...]] = ( + "DYNO", "WEBSITE_INSTANCE_ID", ) HOSTED_IDE_ENV_VARS: Final[tuple[str, ...]] = ( @@ -123,6 +129,7 @@ CONTAINER_ENV_VARS: Final[tuple[str, ...]] = ("KUBERNETES_SERVICE_HOST",) RUNTIME_CONTEXT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = ( ("ci", CI_ENV_VARS), ("serverless", SERVERLESS_ENV_VARS), + ("paas", PAAS_ENV_VARS), ("hosted_ide", HOSTED_IDE_ENV_VARS), ("notebook", NOTEBOOK_ENV_VARS), ("container", CONTAINER_ENV_VARS), diff --git a/lib/crewai/tests/telemetry/test_coding_agent_detection.py b/lib/crewai/tests/telemetry/test_coding_agent_detection.py index a7b81923a..7009d320c 100644 --- a/lib/crewai/tests/telemetry/test_coding_agent_detection.py +++ b/lib/crewai/tests/telemetry/test_coding_agent_detection.py @@ -577,3 +577,36 @@ def test_all_common_attributes_land_on_exported_spans(clean_env, monkeypatch): assert exported["coding_agent"] == "claude_code" assert exported["runtime_context"] == "ci" assert exported["project_id"] == "proj-123" + + +def test_runtime_markers_are_detected_by_presence(clean_env, monkeypatch): + """An empty value still means the platform set the marker. + + Some platforms export a bare `CI=`; truthiness checks would drop those + runs to the TTY fallback and mislabel them as ordinary local executions. + """ + monkeypatch.setattr("os.path.exists", lambda path: False) + clean_env.setenv("CI", "") + + assert detect_runtime_context() == "ci" + + +def test_managed_platforms_are_not_reported_as_serverless(clean_env): + """Long-lived managed platforms must not claim the serverless label. + + DYNO and WEBSITE_INSTANCE_ID mark Heroku dynos and Azure App Service + instances, which are containers rather than per-invocation functions. + """ + clean_env.setenv("DYNO", "web.1") + assert detect_runtime_context() == "paas" + + clean_env.delenv("DYNO") + clean_env.setenv("WEBSITE_INSTANCE_ID", "abc123") + assert detect_runtime_context() == "paas" + + +def test_serverless_markers_still_win_over_paas(clean_env): + clean_env.setenv("DYNO", "web.1") + clean_env.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn") + + assert detect_runtime_context() == "serverless"