diff --git a/docs/edge/ar/telemetry.mdx b/docs/edge/ar/telemetry.mdx index 0cc017ae9..f3869107b 100644 --- a/docs/edge/ar/telemetry.mdx +++ b/docs/edge/ar/telemetry.mdx @@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true' | نعم | بيانات دورة حياة المهمة | تشمل: أوقات الإنشاء وبدء/انتهاء التنفيذ، معرّفات الطاقم والمهمة. مخزنة كنطاقات مع طوابع زمنية. لا بيانات شخصية. | | نعم | سمات LLM | تشمل: الاسم، model_name، model، top_k، temperature، واسم فئة LLM. كلها بيانات تقنية غير شخصية. | | نعم | محاولة نشر الطاقم باستخدام CLI الخاص بـ CrewAI | تشمل: حقيقة إجراء النشر ومعرّف الطاقم، وما إذا كان يحاول سحب السجلات، لا بيانات أخرى. | +| نعم | بيئة التنفيذ | تشمل: مساعد البرمجة بالذكاء الاصطناعي الذي يشغّل العملية إن وُجد (واحد من قائمة ثابتة مثل `claude_code` أو `codex` أو `cursor` أو `unknown`)، ومكان تشغيل العملية (واحد من قائمة ثابتة مثل `ci` أو `container` أو `serverless` أو `interactive`)، و`project_id` من ملف `pyproject.toml` عند ضبطه. يتحقق الاكتشاف فقط مما إذا كانت متغيرات البيئة المعروفة مضبوطة، ولا يقرأ قيمها أبدًا. لا بيانات شخصية. | | لا | بيانات الوكيل الموسّعة | تشمل: وصف الهدف، نص الخلفية، معرّف ملف موجهات i18n. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في حقول النص. | | لا | معلومات المهمة التفصيلية | تشمل: وصف المهمة، وصف المخرجات المتوقعة، مراجع السياق. يجب على المستخدمين التأكد من عدم تضمين معلومات شخصية في هذه الحقول. | | لا | معلومات البيئة | تشمل: المنصة، الإصدار، النظام، الإصدار، وعدد وحدات المعالجة المركزية. مثال: 'Windows 10'، 'x86_64'. لا بيانات شخصية. | diff --git a/docs/edge/en/telemetry.mdx b/docs/edge/en/telemetry.mdx index 1dca7cd06..60e7f1031 100644 --- a/docs/edge/en/telemetry.mdx +++ b/docs/edge/en/telemetry.mdx @@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true' | Yes | Task Lifecycle Data | Includes: creation and execution start/end times, crew and task identifiers. Stored as spans with timestamps. No personal data. | | Yes | LLM Attributes | Includes: name, model_name, model, top_k, temperature, and class name of the LLM. All technical, non-personal data. | | Yes | Crew Deployment attempt using crewAI CLI | Includes: The fact a deploy is being made and crew id, and if it's trying to pull logs, no other data. | +| Yes | Execution Environment | Includes: which AI coding assistant is running the process, if any (one of a fixed list such as `claude_code`, `codex`, `cursor`, or `unknown`), where the process runs (one of a fixed list such as `ci`, `container`, `serverless`, `interactive`), and the `project_id` from your `pyproject.toml` when one is configured. Detection reads only whether known environment variables are set, never their values. No personal data. | | No | Agent's Expanded Data | Includes: goal description, backstory text, i18n prompt file identifier. Users should ensure no personal info is included in text fields. | | No | Detailed Task Information | Includes: task description, expected output description, context references. Users should ensure no personal info is included in these fields. | | No | Environment Information | Includes: platform, release, system, version, and CPU count. Example: 'Windows 10', 'x86_64'. No personal data. | diff --git a/docs/edge/ko/telemetry.mdx b/docs/edge/ko/telemetry.mdx index b706bded2..9ec9397c7 100644 --- a/docs/edge/ko/telemetry.mdx +++ b/docs/edge/ko/telemetry.mdx @@ -45,6 +45,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true' | 예 | 작업 라이프사이클 데이터 | 생성 및 실행 시작/종료 시각, crew 및 작업 식별자가 포함됩니다. 타임스탬프를 포함한 span으로 저장됩니다. 개인 정보 없음. | | 예 | LLM 속성 | LLM의 이름, model_name, 모델, top_k, temperature 및 클래스명이 포함됩니다. 모두 기술적이고 비개인 정보입니다. | | 예 | crewAI CLI를 통한 Crew 배포 시도 | 배포가 시도되고 있고 crew id가 포함되며, 로그를 가져오려고 하는 경우에만 해당. 다른 데이터 없음. | +| 예 | 실행 환경 | 포함: 프로세스를 실행 중인 AI 코딩 어시스턴트(있는 경우, `claude_code`, `codex`, `cursor`, `unknown` 등 고정 목록 중 하나), 프로세스가 실행되는 위치(`ci`, `container`, `serverless`, `interactive` 등 고정 목록 중 하나), 그리고 `pyproject.toml`에 설정된 경우 `project_id`. 감지는 알려진 환경 변수의 설정 여부만 확인하며 값은 읽지 않음. 개인 데이터 없음. | | 아니오 | 에이전트 확장 데이터 | 목표 설명, 배경 이야기 텍스트, i18n 프롬프트 파일 식별자가 포함됩니다. 사용자들은 텍스트 필드에 개인 정보가 포함되지 않도록 해야 합니다. | | 아니오 | 상세 작업 정보 | 작업 설명, 예상 출력 설명, 컨텍스트 참조가 포함됩니다. 사용자들은 이러한 필드에 개인 정보가 포함되지 않도록 해야 합니다. | | 아니오 | 환경 정보 | 플랫폼, 릴리즈, 시스템, 버전, CPU 개수가 포함됩니다. 예: 'Windows 10', 'x86_64'. 개인 정보 없음. | diff --git a/docs/edge/pt-BR/telemetry.mdx b/docs/edge/pt-BR/telemetry.mdx index a53bc30c5..580d8d213 100644 --- a/docs/edge/pt-BR/telemetry.mdx +++ b/docs/edge/pt-BR/telemetry.mdx @@ -46,6 +46,7 @@ os.environ['OTEL_SDK_DISABLED'] = 'true' | Sim | Dados do Ciclo de Vida da Tarefa | Inclui: horários de criação, início/fim de execução, identificadores de crew e tarefa. Armazenado como spans com timestamps. Sem dados pessoais. | | Sim | Atributos do LLM | Inclui: nome, model_name, model, top_k, temperatura e nome da classe do LLM. Todos técnicos, sem dados pessoais. | | Sim | Tentativa de Deploy do Crew pelo CLI do crewAI | Inclui: O fato de um deploy estar sendo realizado e o crew id, e se está tentando buscar logs, sem mais dados. | +| Sim | Ambiente de Execução | Inclui: qual assistente de código com IA está executando o processo, se houver (um de uma lista fixa como `claude_code`, `codex`, `cursor` ou `unknown`), onde o processo é executado (um de uma lista fixa como `ci`, `container`, `serverless`, `interactive`) e o `project_id` do seu `pyproject.toml` quando houver um configurado. A detecção lê apenas se variáveis de ambiente conhecidas estão definidas, nunca seus valores. Sem dados pessoais. | | Não | Dados Expandidos do Agente | Inclui: descrição do objetivo, texto da história, identificador de arquivo i18n prompt. Usuários devem garantir que não haja info pessoal nesses campos de texto. | | Não | Informações Detalhadas da Tarefa | Inclui: descrição da tarefa, descrição do resultado esperado, referências de contexto. Usuários devem garantir que não haja info pessoal nessas áreas. | | Não | Informações de Ambiente | Inclui: plataforma, release, sistema, versão e quantidade de CPUs. Exemplo: 'Windows 10', 'x86_64'. Sem dados pessoais. | diff --git a/lib/crewai/src/crewai/telemetry/telemetry.py b/lib/crewai/src/crewai/telemetry/telemetry.py index ddc5bcfd3..5b91c5af9 100644 --- a/lib/crewai/src/crewai/telemetry/telemetry.py +++ b/lib/crewai/src/crewai/telemetry/telemetry.py @@ -21,6 +21,7 @@ import threading from typing import TYPE_CHECKING, Any import weakref +from crewai_core.project import get_project_id from opentelemetry import trace from opentelemetry.context import Context from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( @@ -54,6 +55,7 @@ from crewai.telemetry.utils import ( add_crew_attributes, close_span, detect_coding_agent, + detect_runtime_context, ) from crewai.utilities.i18n import I18N_DEFAULT from crewai.utilities.logger_utils import suppress_warnings @@ -172,6 +174,7 @@ class Telemetry: # Weak so instrumented apps' providers are not kept alive by telemetry. self._common_attributes_providers: weakref.WeakSet[Any] = weakref.WeakSet() self._common_attributes_lock = threading.Lock() + self._common_attributes: dict[str, str] | None = None if self._is_telemetry_disabled(): return @@ -203,6 +206,39 @@ class Telemetry: raise self.ready = False + def _common_span_attributes(self) -> dict[str, str]: + """Build the attributes every span carries, once per process. + + Memoized because it is computed per provider and reads the project's + ``pyproject.toml``, and because a process cannot change which + assistant, runtime, or project it belongs to partway through. + + Returns: + Attributes to stamp on every span. ``project_id`` is omitted for + projects that do not declare one. + """ + if self._common_attributes is not None: + return self._common_attributes + + attributes = { + "coding_agent": detect_coding_agent(), + "runtime_context": detect_runtime_context(), + } + + try: + # Read-only: minting an id belongs to the CLI commands a user + # invoked, not to a library call during execution. + project_id = get_project_id() + except Exception as e: # Telemetry must never break execution. + logger.debug(f"Failed to read project id: {e}") + project_id = None + + if project_id: + attributes["project_id"] = project_id + + self._common_attributes = attributes + return attributes + def _attach_common_attributes(self, provider: Any) -> None: """Attach process-wide attributes to every span a provider emits. @@ -230,9 +266,7 @@ class Telemetry: if provider in self._common_attributes_providers: return add_span_processor( - CommonAttributesSpanProcessor( - {"coding_agent": detect_coding_agent()} - ) + CommonAttributesSpanProcessor(self._common_span_attributes()) ) self._common_attributes_providers.add(provider) except Exception as e: # Telemetry must never break execution. diff --git a/lib/crewai/src/crewai/telemetry/utils.py b/lib/crewai/src/crewai/telemetry/utils.py index 36c96a385..ec3cafa70 100644 --- a/lib/crewai/src/crewai/telemetry/utils.py +++ b/lib/crewai/src/crewai/telemetry/utils.py @@ -12,7 +12,11 @@ from typing import TYPE_CHECKING, Any, Final from opentelemetry.trace import Span, Status, StatusCode -from crewai.utilities.constants import CODING_AGENT_ENV_MARKERS +from crewai.utilities.constants import ( + CODING_AGENT_ENV_MARKERS, + GENERIC_AGENT_ENV_VARS, + RUNTIME_CONTEXT_ENV_MARKERS, +) if TYPE_CHECKING: @@ -20,23 +24,33 @@ if TYPE_CHECKING: from crewai.task import Task -# Editors whose integrated terminal implies a human is likely present. Used only -# as a weaker fallback when no explicit coding-agent marker is found. +# Editors whose integrated terminal a process can be launched from. Matched on +# an exact value rather than presence, since these variables name the terminal. _EDITOR_TERM_MARKERS: Final[tuple[tuple[str, str, str], ...]] = ( ("TERM_PROGRAM", "vscode", "vscode_terminal"), ("TERMINAL_EMULATOR", "JetBrains-JediTerm", "jetbrains_terminal"), ) -_FALLBACK_AGENT_NAMES: Final[tuple[str, ...]] = ("non_interactive", "unknown") +# Marks a process running inside a container when no more specific runtime +# marker is present. +_DOCKER_ENV_PATH: Final[str] = "/.dockerenv" + +_UNKNOWN: Final[str] = "unknown" +_OTHER: Final[str] = "other" # The complete set of values detect_coding_agent() can ever return. Every value # is a literal from CODING_AGENT_ENV_MARKERS or this module, which is what makes # the function structurally incapable of emitting PII: no environment value, # path, hostname, or user-supplied string can reach the return value. KNOWN_CODING_AGENTS: Final[frozenset[str]] = frozenset( - [name for name, _ in CODING_AGENT_ENV_MARKERS] + [name for name, _ in CODING_AGENT_ENV_MARKERS] + [_OTHER, _UNKNOWN] +) + +# The same guarantee for detect_runtime_context(): a closed set of literals. +KNOWN_RUNTIME_CONTEXTS: Final[frozenset[str]] = frozenset( + [name for name, _ in RUNTIME_CONTEXT_ENV_MARKERS] + [name for _, _, name in _EDITOR_TERM_MARKERS] - + list(_FALLBACK_AGENT_NAMES) + + ["interactive", "non_interactive", _UNKNOWN] ) @@ -57,27 +71,65 @@ def detect_coding_agent() -> str: integrated terminal, so a result names the environment the process is running *under*, not proof that an agent authored the code. + Answers only *which assistant*. Where the process runs is + :func:`detect_runtime_context`, so an "unknown" here is a genuine gap in + the marker table rather than a run that never had an assistant to find. + Returns: - A normalized assistant name (e.g. "claude_code", "cursor", "codex"), - an editor terminal hint (e.g. "vscode_terminal"), "non_interactive" - when no marker is found and there is no TTY, or "unknown" otherwise. - The result is always a member of KNOWN_CODING_AGENTS. + A normalized assistant name (e.g. "claude_code", "cursor", "codex"), or + "unknown" when no marker matches. The result is always a member of + KNOWN_CODING_AGENTS. """ for agent_name, env_vars in CODING_AGENT_ENV_MARKERS: if any(os.environ.get(env_var) for env_var in env_vars): return agent_name - for env_var, expected, agent_name in _EDITOR_TERM_MARKERS: + # Checked last and by presence: the cross-vendor marker establishes that an + # assistant is present without naming one, and an empty value still says so. + if any(env_var in os.environ for env_var in GENERIC_AGENT_ENV_VARS): + return _OTHER + + return _UNKNOWN + + +def detect_runtime_context() -> str: + """Best-effort detection of where this process is running. + + Separate from :func:`detect_coding_agent` because the two answer different + questions. Automated runs -- CI, containers, serverless -- have no + assistant to detect, and reporting them in the assistant field made an + unrecognized assistant indistinguishable from a run that could never have + had one. + + Precedence runs most specific first: CI and hosted IDEs typically run + inside containers, so a bare container match only applies once the more + specific markers have been ruled out. + + Returns: + One of the runtime names (e.g. "ci", "serverless", "container", + "notebook", "hosted_ide"), an editor terminal (e.g. "vscode_terminal"), + "interactive" or "non_interactive" when only a TTY check applies, or + "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(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 agent_name + return context_name + + if os.path.exists(_DOCKER_ENV_PATH): + return "container" try: - if not sys.stdout.isatty(): - return "non_interactive" + return "interactive" if sys.stdout.isatty() else "non_interactive" except (AttributeError, ValueError, OSError): - return "unknown" - - return "unknown" + return _UNKNOWN def add_agent_fingerprint_to_span( diff --git a/lib/crewai/src/crewai/utilities/constants.py b/lib/crewai/src/crewai/utilities/constants.py index bf1c43964..0f31c86aa 100644 --- a/lib/crewai/src/crewai/utilities/constants.py +++ b/lib/crewai/src/crewai/utilities/constants.py @@ -12,15 +12,30 @@ from pydantic_core import CoreSchema __all__ = [ + "ANTIGRAVITY_ENV_VARS", + "AUGMENT_ENV_VARS", "CC_ENV_VAR", + "CC_ENV_VARS", + "CI_ENV_VARS", + "CLINE_ENV_VARS", "CODEX_ENV_VARS", "CODING_AGENT_ENV_MARKERS", + "CONTAINER_ENV_VARS", "CREWAI_TRAINED_AGENTS_FILE_ENV", "CURSOR_ENV_VARS", "EMITTER_COLOR", + "GEMINI_CLI_ENV_VARS", + "GENERIC_AGENT_ENV_VARS", + "HOSTED_IDE_ENV_VARS", + "JUNIE_ENV_VARS", "KNOWLEDGE_DIRECTORY", "MAX_FILE_NAME_LENGTH", + "NOTEBOOK_ENV_VARS", "NOT_SPECIFIED", + "OPENCODE_ENV_VARS", + "PAAS_ENV_VARS", + "RUNTIME_CONTEXT_ENV_MARKERS", + "SERVERLESS_ENV_VARS", "TRAINED_AGENTS_DATA_FILE", "TRAINING_DATA_FILE", ] @@ -35,6 +50,7 @@ CODEX_ENV_VARS: Final[tuple[str, ...]] = ( "CODEX_SANDBOX_NETWORK_DISABLED", "CODEX_THREAD_ID", ) +CC_ENV_VARS: Final[tuple[str, ...]] = (CC_ENV_VAR, "CLAUDE_CODE") CURSOR_ENV_VARS: Final[tuple[str, ...]] = ( "CURSOR_AGENT", "CURSOR_EXTENSION_HOST_ROLE", @@ -42,6 +58,20 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = ( "CURSOR_TRACE_ID", "CURSOR_WORKSPACE_LABEL", ) +ANTIGRAVITY_ENV_VARS: Final[tuple[str, ...]] = ( + "ANTIGRAVITY_AGENT", + "ANTIGRAVITY_CLI_ALIAS", +) +AUGMENT_ENV_VARS: Final[tuple[str, ...]] = ("AUGMENT_AGENT",) +CLINE_ENV_VARS: Final[tuple[str, ...]] = ("CLINE_ACTIVE",) +GEMINI_CLI_ENV_VARS: Final[tuple[str, ...]] = ("GEMINI_CLI",) +JUNIE_ENV_VARS: Final[tuple[str, ...]] = ("JUNIE_DATA", "JUNIE_SHIM_PATH") +OPENCODE_ENV_VARS: Final[tuple[str, ...]] = ("OPENCODE", "OPENCODE_CLIENT") + +# Proposed cross-vendor marker (agentsmd/agents.md#136). Checked last and +# reported as "other": it says an assistant is present without naming one, and +# reading its value to find out would put an arbitrary string in telemetry. +GENERIC_AGENT_ENV_VARS: Final[tuple[str, ...]] = ("AI_AGENT",) # Ordered (name, env vars) pairs for identifying the AI coding assistant a # process is running under. Reuses the sets above and keeps the same precedence @@ -62,13 +92,99 @@ CURSOR_ENV_VARS: Final[tuple[str, ...]] = ( # a leftover config value would mislabel ordinary human executions. # # Extend the shared sets above rather than adding a parallel tuple here, so both -# detection paths pick the new markers up together. +# detection paths pick the new markers up together: ``get_env_context()`` walks +# this same table for its precedence and emits ``DefaultEnvEvent`` for the +# assistants that have no event class of their own. +# +# Markers below the first three were taken from the published detection matrix +# at vercel/detect-agent (agents.json), cross-checked against the proposal in +# agentsmd/agents.md#136 and microsoft/vscode#311734. Rule 2 excluded several +# entries those sources list: Goose's ``GOOSE_PROVIDER`` and Copilot's +# ``COPILOT_MODEL`` / ``COPILOT_GITHUB_TOKEN`` are user configuration, and a +# committed ``.env`` carrying one would relabel every ordinary run. Replit's +# ``REPL_ID`` is a hosted environment rather than an assistant, so it stays in +# HOSTED_IDE_ENV_VARS. +# +# The assistants that spawn inside another editor's terminal are ordered ahead +# of Cursor for the same reason Codex is: CURSOR_* is set for every integrated +# terminal, so checking Cursor first would mask anything running inside it. CODING_AGENT_ENV_MARKERS: Final[tuple[tuple[str, tuple[str, ...]], ...]] = ( - ("claude_code", (CC_ENV_VAR,)), + ("claude_code", CC_ENV_VARS), ("codex", CODEX_ENV_VARS), + ("cline", CLINE_ENV_VARS), + ("gemini_cli", GEMINI_CLI_ENV_VARS), + ("augment", AUGMENT_ENV_VARS), + ("opencode", OPENCODE_ENV_VARS), + ("antigravity", ANTIGRAVITY_ENV_VARS), + ("junie", JUNIE_ENV_VARS), ("cursor", CURSOR_ENV_VARS), ) +# Markers for *where* a process runs, kept separate from which assistant is +# driving it. The two answer different questions: a scheduled container run has +# no assistant to detect, and folding it into the assistant field made "no +# marker found" and "no assistant possible" indistinguishable. +# +# These are published platform contracts rather than per-tool observations, so +# they do not need the case-by-case verification the assistant table requires. +# Presence is all that is checked; no value is ever read. +CI_ENV_VARS: Final[tuple[str, ...]] = ( + "APPVEYOR", + "BITBUCKET_BUILD_NUMBER", + "BUILDKITE", + "CI", + "CIRCLECI", + "DRONE", + "GITHUB_ACTIONS", + "GITLAB_CI", + "JENKINS_URL", + "TEAMCITY_VERSION", + "TF_BUILD", + "TRAVIS", +) +SERVERLESS_ENV_VARS: Final[tuple[str, ...]] = ( + "AWS_LAMBDA_FUNCTION_NAME", + "FUNCTIONS_EXTENSION_VERSION", + "FUNCTIONS_WORKER_RUNTIME", + "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. +# +# Azure Functions run on the App Service host and inherit WEBSITE_INSTANCE_ID, +# so they would land here despite being serverless. The FUNCTIONS_* markers +# above are checked first to keep them out. +PAAS_ENV_VARS: Final[tuple[str, ...]] = ( + "DYNO", + "WEBSITE_INSTANCE_ID", +) +HOSTED_IDE_ENV_VARS: Final[tuple[str, ...]] = ( + "CODESPACES", + "GITPOD_WORKSPACE_ID", + "REPL_ID", +) +NOTEBOOK_ENV_VARS: Final[tuple[str, ...]] = ( + "COLAB_RELEASE_TAG", + "JPY_PARENT_PID", +) +CONTAINER_ENV_VARS: Final[tuple[str, ...]] = ("KUBERNETES_SERVICE_HOST",) + +# Ordered most specific first. CI jobs and hosted IDEs usually run inside +# containers, so a bare container match is only meaningful once the others have +# been ruled out. +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), +) + class _NotSpecified: """Sentinel class to detect when no value has been explicitly provided. diff --git a/lib/crewai/src/crewai/utilities/env.py b/lib/crewai/src/crewai/utilities/env.py index af77faefc..48c1fc6e4 100644 --- a/lib/crewai/src/crewai/utilities/env.py +++ b/lib/crewai/src/crewai/utilities/env.py @@ -8,20 +8,21 @@ from crewai.events.types.env_events import ( CursorEnvEvent, DefaultEnvEvent, ) -from crewai.utilities.constants import CC_ENV_VAR, CODEX_ENV_VARS, CURSOR_ENV_VARS +from crewai.utilities.constants import CODING_AGENT_ENV_MARKERS _env_context_emitted: contextvars.ContextVar[bool] = contextvars.ContextVar( "_env_context_emitted", default=False ) - -def _is_codex_env() -> bool: - return any(os.environ.get(var) for var in CODEX_ENV_VARS) - - -def _is_cursor_env() -> bool: - return any(os.environ.get(var) for var in CURSOR_ENV_VARS) +# Assistants with an event of their own. Anything else in the shared table is +# detected with the same precedence but reported as DefaultEnvEvent, so the two +# detection paths can never disagree about which assistant is present. +_AGENT_EVENTS = { + "claude_code": CCEnvEvent, + "codex": CodexEnvEvent, + "cursor": CursorEnvEvent, +} def get_env_context() -> None: @@ -29,11 +30,13 @@ def get_env_context() -> None: return _env_context_emitted.set(True) - if os.environ.get(CC_ENV_VAR): - crewai_event_bus.emit(None, CCEnvEvent()) - elif _is_codex_env(): - crewai_event_bus.emit(None, CodexEnvEvent()) - elif _is_cursor_env(): - crewai_event_bus.emit(None, CursorEnvEvent()) - else: - crewai_event_bus.emit(None, DefaultEnvEvent()) + # Walks the shared table rather than restating precedence: hard-coding the + # order here let the two paths drift, so a marker added for telemetry was + # invisible to this one. + for agent_name, env_vars in CODING_AGENT_ENV_MARKERS: + if any(os.environ.get(var) for var in env_vars): + event = _AGENT_EVENTS.get(agent_name, DefaultEnvEvent) + crewai_event_bus.emit(None, event()) + return + + crewai_event_bus.emit(None, DefaultEnvEvent()) diff --git a/lib/crewai/tests/telemetry/test_coding_agent_detection.py b/lib/crewai/tests/telemetry/test_coding_agent_detection.py index 087ad2920..9c6280815 100644 --- a/lib/crewai/tests/telemetry/test_coding_agent_detection.py +++ b/lib/crewai/tests/telemetry/test_coding_agent_detection.py @@ -5,20 +5,47 @@ from unittest.mock import patch import pytest -from crewai.telemetry.utils import KNOWN_CODING_AGENTS, detect_coding_agent +from crewai.telemetry.utils import ( + KNOWN_CODING_AGENTS, + KNOWN_RUNTIME_CONTEXTS, + detect_coding_agent, + detect_runtime_context, +) from crewai.utilities.constants import ( + ANTIGRAVITY_ENV_VARS, + AUGMENT_ENV_VARS, CC_ENV_VAR, + CC_ENV_VARS, + CLINE_ENV_VARS, CODEX_ENV_VARS, CODING_AGENT_ENV_MARKERS, CURSOR_ENV_VARS, + GEMINI_CLI_ENV_VARS, + GENERIC_AGENT_ENV_VARS, + JUNIE_ENV_VARS, + OPENCODE_ENV_VARS, + RUNTIME_CONTEXT_ENV_MARKERS, ) -# Derived from the shared table rather than restated, so adding an assistant -# there cannot leave these tests silently checking a stale marker set. -ALL_MARKERS = tuple( - var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars -) + ("TERM_PROGRAM", "TERMINAL_EMULATOR") +# Derived from the shared tables rather than restated, so adding an assistant +# or runtime there cannot leave these tests silently checking a stale set. +RUNTIME_MARKERS = tuple( + var for _, env_vars in RUNTIME_CONTEXT_ENV_MARKERS for var in env_vars +) + +ALL_MARKERS = ( + tuple(var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars) + + RUNTIME_MARKERS + + GENERIC_AGENT_ENV_VARS + + ("TERM_PROGRAM", "TERMINAL_EMULATOR") +) + +EVERY_RUNTIME_CASE = [ + (var, context) + for context, env_vars in RUNTIME_CONTEXT_ENV_MARKERS + for var in env_vars +] EVERY_MARKER_CASE = [ (var, agent) for agent, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars @@ -33,6 +60,18 @@ def clean_env(monkeypatch): return monkeypatch +@pytest.fixture +def otel_enabled(monkeypatch): + """Let the SDK build real spans for tests that assert on exported ones. + + The suite runs with OTEL_SDK_DISABLED set, which makes TracerProvider hand + out no-op tracers. An export-based assertion then sees zero spans rather + than a failed attribute, so it fails only when it happens to run before a + test whose fixture flips the variable - which random ordering decides. + """ + monkeypatch.setenv("OTEL_SDK_DISABLED", "false") + + @pytest.fixture def isolated_telemetry(monkeypatch): """Build a fresh Telemetry without touching the process-wide singleton. @@ -143,15 +182,33 @@ def test_precedence_matches_get_env_context(clean_env): assert detect_coding_agent() == expected, markers -def test_config_style_variables_are_not_used_as_markers(): +@pytest.mark.parametrize( + "config_var", + [ + "AIDER_MODEL", + "COPILOT_GITHUB_TOKEN", + "COPILOT_MODEL", + "GOOSE_PROVIDER", + ], +) +def test_config_style_variables_are_not_used_as_markers(config_var): """Persistent user config must never be treated as a session marker. crewai loads dotenv files on normal runs, so a committed AIDER_MODEL or - similar would mislabel ordinary human executions. + GOOSE_PROVIDER would mislabel ordinary human executions. Published + detection matrices list several of these; they are deliberately excluded + here rather than copied wholesale. """ all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars} - assert "AIDER_MODEL" not in all_vars + assert config_var not in all_vars + + +def test_hosted_environments_are_not_reported_as_assistants(): + """REPL_ID marks a hosted environment, not an assistant driving the run.""" + all_vars = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars} + + assert "REPL_ID" not in all_vars def test_every_marker_comes_from_a_verified_set(): @@ -162,15 +219,63 @@ def test_every_marker_comes_from_a_verified_set(): Adding an assistant means extending the canonical sets, which keeps both detection paths in sync. """ - verified = {CC_ENV_VAR, *CODEX_ENV_VARS, *CURSOR_ENV_VARS} + verified = { + *ANTIGRAVITY_ENV_VARS, + *AUGMENT_ENV_VARS, + *CC_ENV_VARS, + *CLINE_ENV_VARS, + *CODEX_ENV_VARS, + *CURSOR_ENV_VARS, + *GEMINI_CLI_ENV_VARS, + *JUNIE_ENV_VARS, + *OPENCODE_ENV_VARS, + } declared = {var for _, env_vars in CODING_AGENT_ENV_MARKERS for var in env_vars} assert declared == verified, ( - "markers must come from CC_ENV_VAR / CODEX_ENV_VARS / CURSOR_ENV_VARS; " + "markers must come from the canonical per-assistant sets; " f"unverified names present: {sorted(declared - verified)}" ) +def test_generic_marker_is_the_last_resort(clean_env): + """AI_AGENT says an assistant is present without naming which one. + + A named marker must win, so the generic entry cannot mask a specific one. + """ + clean_env.setenv("AI_AGENT", "1") + assert detect_coding_agent() == "other" + + clean_env.setenv("CLINE_ACTIVE", "true") + assert detect_coding_agent() == "cline" + + +def test_generic_marker_value_is_never_reported(clean_env): + """Its value is an arbitrary vendor string, so it is never read.""" + clean_env.setenv("AI_AGENT", "some-unreleased-tool/2.0") + + assert detect_coding_agent() == "other" + + +def test_terminal_bound_assistants_outrank_cursor(clean_env): + """CURSOR_* is set for every integrated terminal. + + Checking Cursor first would report cursor for anything spawned inside it, + the same trap Codex already had to be ordered around. + """ + clean_env.setenv("CURSOR_TRACE_ID", "t-1") + + for marker, expected in ( + ("CLINE_ACTIVE", "cline"), + ("GEMINI_CLI", "gemini_cli"), + ("AUGMENT_AGENT", "augment"), + ("OPENCODE_CLIENT", "opencode"), + ): + clean_env.setenv(marker, "1") + assert detect_coding_agent() == expected, marker + clean_env.delenv(marker) + + def test_concurrent_attach_registers_the_processor_once(isolated_telemetry, clean_env): """Check-then-act on the provider set must be locked. @@ -216,16 +321,16 @@ def test_concurrent_attach_registers_the_processor_once(isolated_telemetry, clea def test_editor_terminal_requires_exact_value(clean_env): clean_env.setenv("TERM_PROGRAM", "vscode") - assert detect_coding_agent() == "vscode_terminal" + assert detect_runtime_context() == "vscode_terminal" clean_env.setenv("TERM_PROGRAM", "iTerm.app") - assert detect_coding_agent() != "vscode_terminal" + assert detect_runtime_context() != "vscode_terminal" -def test_explicit_agent_marker_wins_over_editor_terminal(clean_env): +def test_editor_terminal_is_not_reported_as_an_assistant(clean_env): + """An editor's terminal says where a process runs, not who drove it.""" clean_env.setenv("TERM_PROGRAM", "vscode") - clean_env.setenv("CLAUDECODE", "1") - assert detect_coding_agent() == "claude_code" + assert detect_coding_agent() == "unknown" def test_empty_marker_value_is_ignored(clean_env): @@ -233,12 +338,61 @@ def test_empty_marker_value_is_ignored(clean_env): assert detect_coding_agent() != "claude_code" +@pytest.mark.parametrize(("env_var", "expected"), EVERY_RUNTIME_CASE) +def test_detects_every_runtime_marker(clean_env, env_var, expected): + """Every runtime marker must map to its context.""" + clean_env.setenv(env_var, "1") + assert detect_runtime_context() == expected + + +def test_runtime_precedence_prefers_the_most_specific(clean_env): + """CI and hosted IDEs usually run in containers; the specific one wins.""" + clean_env.setenv("KUBERNETES_SERVICE_HOST", "10.0.0.1") + assert detect_runtime_context() == "container" + + clean_env.setenv("GITHUB_ACTIONS", "true") + assert detect_runtime_context() == "ci" + + +def test_an_automated_run_still_reports_an_unknown_assistant(clean_env): + """The split must keep the two fields independent. + + A CI run has no assistant to find, which is different from failing to + recognize one - the reason they no longer share a field. + """ + clean_env.setenv("CI", "true") + assert detect_runtime_context() == "ci" + assert detect_coding_agent() == "unknown" + + +def test_assistant_and_runtime_are_reported_together(clean_env): + """An assistant inside CI must not mask either signal.""" + clean_env.setenv("CI", "true") + clean_env.setenv("CLAUDECODE", "1") + assert detect_coding_agent() == "claude_code" + assert detect_runtime_context() == "ci" + + def test_falls_back_to_non_interactive_without_tty(clean_env, monkeypatch): + monkeypatch.setattr("os.path.exists", lambda path: False) monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: False})()) - assert detect_coding_agent() == "non_interactive" + assert detect_runtime_context() == "non_interactive" -def test_falls_back_to_unknown_with_tty(clean_env, monkeypatch): +def test_falls_back_to_interactive_with_tty(clean_env, monkeypatch): + monkeypatch.setattr("os.path.exists", lambda path: False) + monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: True})()) + assert detect_runtime_context() == "interactive" + + +def test_dockerenv_marks_a_container(clean_env, monkeypatch): + """The container check is the last resort before the TTY fallback.""" + monkeypatch.setattr("os.path.exists", lambda path: path == "/.dockerenv") + assert detect_runtime_context() == "container" + + +def test_unmatched_assistant_is_unknown(clean_env, monkeypatch): + """No marker means a gap in the table, reported as unknown.""" monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: True})()) assert detect_coding_agent() == "unknown" @@ -255,8 +409,9 @@ def test_handles_broken_stdout(clean_env, monkeypatch): def isatty(self): raise ValueError("detached") + monkeypatch.setattr("os.path.exists", lambda path: False) monkeypatch.setattr("sys.stdout", BrokenStdout()) - assert detect_coding_agent() == "unknown" + assert detect_runtime_context() == "unknown" def test_result_is_always_a_known_literal(clean_env): @@ -269,20 +424,23 @@ def test_result_is_always_a_known_literal(clean_env): for var in ALL_MARKERS: clean_env.setenv(var, sensitive) - result = detect_coding_agent() - assert result in KNOWN_CODING_AGENTS - assert sensitive not in result + agent = detect_coding_agent() + context = detect_runtime_context() + assert agent in KNOWN_CODING_AGENTS + assert context in KNOWN_RUNTIME_CONTEXTS + assert sensitive not in agent + assert sensitive not in context clean_env.delenv(var, raising=False) def test_known_agents_contains_no_pii_shaped_values(): """Every possible emitted value is a short, opaque identifier.""" - for name in KNOWN_CODING_AGENTS: + for name in KNOWN_CODING_AGENTS | KNOWN_RUNTIME_CONTEXTS: assert name.replace("_", "").isalnum(), name assert len(name) <= 32, name -def test_coding_agent_lands_on_every_exported_span(clean_env): +def test_coding_agent_lands_on_every_exported_span(clean_env, otel_enabled): """End-to-end: the attribute must appear as a *span attribute* on any span. It cannot be a Resource attribute - the ingestion pipeline preserves only @@ -319,7 +477,7 @@ def test_coding_agent_lands_on_every_exported_span(clean_env): assert "coding_agent" not in exported[0].resource.attributes -def test_common_attributes_processor_never_breaks_span_creation(clean_env): +def test_common_attributes_processor_never_breaks_span_creation(clean_env, otel_enabled): """A failure applying attributes must not propagate into user execution.""" from crewai.telemetry.telemetry import CommonAttributesSpanProcessor @@ -348,7 +506,7 @@ def test_coding_agent_span_emits_once(isolated_telemetry, clean_env, monkeypatch def test_attribute_survives_an_externally_installed_provider( - isolated_telemetry, clean_env + isolated_telemetry, clean_env, otel_enabled ): """Spans must keep coding_agent when the app installs its own provider. @@ -402,3 +560,219 @@ def test_attaching_to_a_provider_without_processors_is_safe(isolated_telemetry): telemetry = isolated_telemetry() telemetry._attach_common_attributes(object()) + + +def _common_attributes(monkeypatch, project_id=None): + """Build the process-wide span attributes with a stubbed project id.""" + from crewai.telemetry.telemetry import Telemetry + + monkeypatch.setattr( + "crewai.telemetry.telemetry.get_project_id", lambda *a, **k: project_id + ) + telemetry = Telemetry.__new__(Telemetry) + telemetry._common_attributes = None + return telemetry._common_span_attributes() + + +def test_common_attributes_carry_agent_and_runtime(clean_env, monkeypatch): + """Both fields ride on every span, independently of each other.""" + clean_env.setenv("CLAUDECODE", "1") + clean_env.setenv("GITHUB_ACTIONS", "true") + + attributes = _common_attributes(monkeypatch) + + assert attributes["coding_agent"] == "claude_code" + assert attributes["runtime_context"] == "ci" + + +def test_common_attributes_include_project_id_when_declared(clean_env, monkeypatch): + attributes = _common_attributes(monkeypatch, project_id="proj-123") + + assert attributes["project_id"] == "proj-123" + + +def test_project_id_is_omitted_when_absent(clean_env, monkeypatch): + """Projects without an id must not report a placeholder.""" + attributes = _common_attributes(monkeypatch, project_id=None) + + assert "project_id" not in attributes + + +def test_project_id_lookup_never_breaks_telemetry(clean_env, monkeypatch): + """A failed lookup degrades to omitting the attribute.""" + from crewai.telemetry.telemetry import Telemetry + + def boom(*args, **kwargs): + raise OSError("unreadable") + + monkeypatch.setattr("crewai.telemetry.telemetry.get_project_id", boom) + telemetry = Telemetry.__new__(Telemetry) + telemetry._common_attributes = None + + attributes = telemetry._common_span_attributes() + + assert "project_id" not in attributes + assert "coding_agent" in attributes + + +def test_common_attributes_are_computed_once(clean_env, monkeypatch): + """The project file must not be re-read for each provider.""" + from crewai.telemetry.telemetry import Telemetry + + calls = [] + + def counting_get_project_id(*args, **kwargs): + calls.append(1) + return "proj-123" + + monkeypatch.setattr( + "crewai.telemetry.telemetry.get_project_id", counting_get_project_id + ) + telemetry = Telemetry.__new__(Telemetry) + telemetry._common_attributes = None + + first = telemetry._common_span_attributes() + second = telemetry._common_span_attributes() + + assert first is second + assert len(calls) == 1 + + +def test_all_common_attributes_land_on_exported_spans(clean_env, monkeypatch, otel_enabled): + """End-to-end: every common attribute survives onto arbitrary spans.""" + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + + from crewai.telemetry.telemetry import CommonAttributesSpanProcessor + + clean_env.setenv("CLAUDECODE", "1") + clean_env.setenv("CI", "true") + attributes = _common_attributes(monkeypatch, project_id="proj-123") + + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(CommonAttributesSpanProcessor(attributes)) + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + provider.get_tracer("test").start_span("Feature Usage").end() + provider.force_flush() + + exported = dict(exporter.get_finished_spans()[0].attributes) + 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" + + +def test_generic_marker_is_detected_by_presence(clean_env): + """An empty AI_AGENT still says an assistant is present. + + The named markers keep truthiness, where an empty value means the tool set + a placeholder rather than claiming the session. + """ + clean_env.setenv("AI_AGENT", "") + + assert detect_coding_agent() == "other" + + +def test_azure_functions_are_not_reported_as_paas(clean_env): + """Azure Functions run on the App Service host and inherit its marker.""" + clean_env.setenv("WEBSITE_INSTANCE_ID", "abc123") + clean_env.setenv("FUNCTIONS_WORKER_RUNTIME", "python") + + assert detect_runtime_context() == "serverless" + + +def test_env_context_precedence_matches_the_shared_table(clean_env): + """Both detection paths must agree on which assistant is present. + + get_env_context previously restated precedence, so a marker added for + telemetry was invisible here and the two disagreed. + """ + from crewai.events.types.env_events import ( + CCEnvEvent, + CodexEnvEvent, + CursorEnvEvent, + DefaultEnvEvent, + ) + from crewai.utilities import env as env_module + + agent_to_event = { + "claude_code": CCEnvEvent, + "codex": CodexEnvEvent, + "cursor": CursorEnvEvent, + } + + for agent, env_vars in CODING_AGENT_ENV_MARKERS: + for var in env_vars: + for other in ALL_MARKERS: + clean_env.delenv(other, raising=False) + clean_env.setenv(var, "1") + + emitted: list[type] = [] + clean_env.setattr( + env_module.crewai_event_bus, + "emit", + lambda _source, event, sink=emitted: sink.append(type(event)), + ) + env_module._env_context_emitted.set(False) + env_module.get_env_context() + + assert detect_coding_agent() == agent, var + assert emitted[0] is agent_to_event.get(agent, DefaultEnvEvent), var + + +def test_assistant_inside_cursor_agrees_across_both_paths(clean_env): + """Cursor sets CURSOR_* in every terminal, including for other assistants.""" + from crewai.events.types.env_events import DefaultEnvEvent + from crewai.utilities import env as env_module + + clean_env.setenv("CURSOR_TRACE_ID", "t-1") + clean_env.setenv("CLINE_ACTIVE", "true") + + emitted: list[type] = [] + clean_env.setattr( + env_module.crewai_event_bus, + "emit", + lambda _source, event, sink=emitted: sink.append(type(event)), + ) + env_module._env_context_emitted.set(False) + env_module.get_env_context() + + assert detect_coding_agent() == "cline" + assert emitted[0] is DefaultEnvEvent diff --git a/pyproject.toml b/pyproject.toml index 4b64275e9..970d5c69e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,7 +172,7 @@ info = "Commits must follow Conventional Commits 1.0.0." [tool.uv] exclude-newer = "3 days" # These security fixes are newer than the global supply-chain cutoff. -exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z", gitpython = "2026-07-27T00:00:00Z" } +exclude-newer-package = { pypdf = "2026-08-07T00:00:00Z", msgpack = "2026-06-20T00:00:00Z", pydantic-settings = "2026-06-20T00:00:00Z", langsmith = "2026-06-20T00:00:00Z", gitpython = "2026-08-05T00:00:00Z" } # composio-core pins rich<14 but textual requires rich>=14. # onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10. @@ -186,6 +186,9 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T # cryptography <50.0.0 has GHSA-g6cj-pr64-35w5 (PKCS#7 Bleichenbacher oracle); force 50.0.0+. # pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p. # pypdf <6.14.2 has GHSA-jm82-fx9c-mx94 and GHSA-5qjq-93h5-hrgp/GHSA-55h5-xmcq-c37v/GHSA-g867-7843-wf8q/GHSA-5xf7-4p34-54qr; force 6.14.2+. +# pypdf <6.15.0 has GHSA-fwg2-594c-jp42 and GHSA-fp3f-mc75-235c (unbounded runtime/memory on large content +# and /ToUnicode streams); force 6.15.0+. Its exclude-newer-package cutoff is bumped to 2026-08-07 to admit +# that release. # uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+. # python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers). # gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath). @@ -194,7 +197,11 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T # gitpython 3.1.52 has GHSA-3rp5-jjmw-4wv2, GHSA-fjr4-x663-mwxc, GHSA-6p8h-3wgx-97gf, and GHSA-r9mr-m37c-5fr3; force 3.1.55+. # gitpython <3.1.56 has GHSA-p538-c434-8v24 (arbitrary file truncation via `git rev-list --output` argument # injection) and <3.1.57 has GHSA-3f7w-8rr8-f37f (unguarded git option forwarding in IndexFile.checkout and -# TagReference); force 3.1.57+. Its exclude-newer-package cutoff is bumped to 2026-07-27 to admit that release. +# TagReference); force 3.1.57+. +# gitpython <3.1.58 has GHSA-9rj7-rf2p-w77r, GHSA-4gmw-gg2m-w46p, GHSA-hh9p-6wh2-4mfc, GHSA-wvpp-8hx9-p66j and +# GHSA-jm78-9fvv-mhgr (further unguarded git option forwarding in Repo.init, read-tree and git-config, plus +# arbitrary file read via --pathspec-from-file); force 3.1.58+. Its exclude-newer-package cutoff is bumped to +# 2026-08-05 to admit that release. # pyasn1 <0.6.4 has GHSA-8ppf-4f7h-5ppj and GHSA-hm4w-wwcw-mr6r; force 0.6.4+. # urllib3 <2.7.0 has GHSA-qccp-gfcp-xxvc (ProxyManager cross-origin redirect leaks Authorization/Cookie) and GHSA-mf9v-mfxr-j63j (streaming decompression-bomb bypass); force 2.7.0+. # langsmith <0.8.18 has GHSA-3644-q5cj-c5c7 (public prompt manifest deserialization, SSRF/secret disclosure) @@ -231,10 +238,10 @@ override-dependencies = [ "urllib3>=2.7.0", "transformers>=5.4.0; python_version >= '3.10'", "cryptography>=50.0.0", - "pypdf>=6.14.2,<7", + "pypdf>=6.15.0,<7", "uv>=0.11.15,<1", "python-multipart>=0.0.27,<1", - "gitpython>=3.1.57,<4", + "gitpython>=3.1.58,<4", "pyasn1>=0.6.4", "langsmith>=0.8.18,<1", "authlib>=1.6.12", diff --git a/uv.lock b/uv.lock index 92440253e..00cb0e056 100644 --- a/uv.lock +++ b/uv.lock @@ -19,8 +19,8 @@ exclude-newer-span = "P3D" [options.exclude-newer-package] msgpack = "2026-06-20T00:00:00Z" langsmith = "2026-06-20T00:00:00Z" -gitpython = "2026-07-27T00:00:00Z" -pypdf = "2026-06-24T00:00:00Z" +gitpython = "2026-08-05T00:00:00Z" +pypdf = "2026-08-07T00:00:00Z" pydantic-settings = "2026-06-20T00:00:00Z" [manifest] @@ -37,7 +37,7 @@ overrides = [ { name = "authlib", specifier = ">=1.6.12" }, { name = "cryptography", specifier = ">=50.0.0" }, { name = "docling-core", extras = ["chunking"], specifier = ">=2.74.1" }, - { name = "gitpython", specifier = ">=3.1.57,<4" }, + { name = "gitpython", specifier = ">=3.1.58,<4" }, { name = "h2", specifier = ">=4.4.1" }, { name = "langchain-core", specifier = ">=1.3.3,<2" }, { name = "langchain-text-splitters", specifier = ">=1.1.2,<2" }, @@ -51,7 +51,7 @@ overrides = [ { name = "pip", specifier = ">=26.1.2" }, { name = "pyasn1", specifier = ">=0.6.4" }, { name = "pydantic-settings", specifier = ">=2.14.2" }, - { name = "pypdf", specifier = ">=6.14.2,<7" }, + { name = "pypdf", specifier = ">=6.15.0,<7" }, { name = "python-multipart", specifier = ">=0.0.27,<1" }, { name = "rich", specifier = ">=13.7.1" }, { name = "setuptools", specifier = ">=83.0.0" }, @@ -1057,7 +1057,7 @@ name = "coloredlogs" version = "15.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "humanfriendly" }, + { name = "humanfriendly", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ @@ -1155,7 +1155,7 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 's390x'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1230,7 +1230,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and platform_machine == 's390x'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1884,34 +1884,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufft = [ - { name = "nvidia-cufft" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufile = [ - { name = "nvidia-cufile" }, + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] curand = [ - { name = "nvidia-curand" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusolver = [ - { name = "nvidia-cusolver" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusparse = [ - { name = "nvidia-cusparse" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvtx = [ - { name = "nvidia-nvtx" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] @@ -2436,7 +2436,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2772,14 +2772,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.57" +version = "3.1.58" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22", size = 228498, upload-time = "2026-08-04T15:05:49.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f", size = 220183, upload-time = "2026-08-04T15:05:48.025Z" }, ] [[package]] @@ -3025,8 +3025,8 @@ name = "grpcio-health-checking" version = "1.71.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, + { name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "protobuf", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/86/20994347ef36b7626fb74539f13128100dd8b7eaac67efc063264e6cdc80/grpcio_health_checking-1.71.2.tar.gz", hash = "sha256:1c21ece88c641932f432b573ef504b20603bdf030ad4e1ec35dd7fdb4ea02637", size = 16770, upload-time = "2025-06-28T04:24:08.768Z" } wheels = [ @@ -3230,7 +3230,7 @@ name = "humanfriendly" version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ @@ -4467,10 +4467,10 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 's390x'", ] dependencies = [ - { name = "jsonref" }, - { name = "mcp", extra = ["ws"] }, - { name = "pydantic" }, - { name = "python-dotenv" }, + { name = "jsonref", marker = "python_full_version < '3.12'" }, + { name = "mcp", extra = ["ws"], marker = "python_full_version < '3.12'" }, + { name = "pydantic", marker = "python_full_version < '3.12'" }, + { name = "python-dotenv", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/28/64fc666fa5d86bb1b048c167975d4ea19210f9f8571b64b26563739774ac/mcpadapt-0.1.19.tar.gz", hash = "sha256:dfab84fc75cc84a49a40bd61079773b1faf840227b74b82c71a7755b9c1957c5", size = 4227721, upload-time = "2025-10-16T07:11:56.736Z" } wheels = [ @@ -4488,10 +4488,10 @@ resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 's390x'", ] dependencies = [ - { name = "jsonref" }, - { name = "mcp", extra = ["ws"] }, - { name = "pydantic" }, - { name = "python-dotenv" }, + { name = "jsonref", marker = "python_full_version >= '3.12'" }, + { name = "mcp", extra = ["ws"], marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/71/1bbbe157e55d30ab4a74fa878f6942cc0586e9820f03e03451a3d2297e9b/mcpadapt-0.1.20.tar.gz", hash = "sha256:4047c0da61e481dd0673a48936a427da9e6547c6cf0d580ff4e4761dcf058ed1", size = 4203656, upload-time = "2025-10-24T15:35:02.135Z" } wheels = [ @@ -5507,12 +5507,12 @@ name = "onnxruntime" version = "1.23.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, - { name = "flatbuffers" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, + { name = "coloredlogs", marker = "python_full_version < '3.11'" }, + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, @@ -7187,14 +7187,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.14.2" +version = "6.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/17/ee75a92718ec7212de831e71454d702225aa5e474a805cce169806044453/pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79", size = 6993794, upload-time = "2026-08-06T13:06:49.929Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/ce3067ac31e214a66388159f8462ddb8c13dd00170f24d555a1f1ae8ee91/pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee", size = 378123, upload-time = "2026-08-06T13:06:47.709Z" }, ] [[package]] @@ -8160,7 +8160,7 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 's390x'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -8224,7 +8224,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and platform_machine == 's390x'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -9855,13 +9855,13 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine == 's390x'", ] dependencies = [ - { name = "authlib" }, - { name = "deprecation" }, - { name = "grpcio" }, - { name = "grpcio-health-checking" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "validators" }, + { name = "authlib", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "deprecation", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "grpcio", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "grpcio-health-checking", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "httpx", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "pydantic", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, + { name = "validators", marker = "python_full_version < '3.11' or (python_full_version >= '3.13' and platform_machine != 's390x')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/b9/7b9e05cf923743aa1479afcd85c48ebca82d031c3c3a5d02b1b3fcb52eb9/weaviate_client-4.16.2.tar.gz", hash = "sha256:eb7107a3221a5ad68d604cafc65195bd925a9709512ea0b6fe0dd212b0678fab", size = 681321, upload-time = "2025-07-22T09:10:48.79Z" } wheels = [ @@ -9880,13 +9880,13 @@ resolution-markers = [ "python_full_version == '3.11.*' and platform_machine == 's390x'", ] dependencies = [ - { name = "authlib" }, - { name = "grpcio" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "validators" }, + { name = "authlib", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "grpcio", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "httpx", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "protobuf", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "pydantic", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, + { name = "validators", marker = "(python_full_version >= '3.11' and python_full_version < '3.13') or (python_full_version >= '3.11' and platform_machine == 's390x')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/b8/103f3aaa246d4e932f4cfeb846e51436966f2aeedf60c2665a3fc51a975a/weaviate_client-4.21.3.tar.gz", hash = "sha256:d7b1f2b0cecbc747e9427f4e3b9463cdfee090746bfbbd40e59cfa25ea2afd4a", size = 847895, upload-time = "2026-06-02T13:03:51.598Z" } wheels = [