mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
feat: split runtime context from coding agent, add project id (#6867)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / Detect changes (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / Detect changes (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* feat(telemetry): split runtime context from coding agent, add project id The coding-agent field answered two questions at once. A run with no TTY reported "non_interactive" and an editor's integrated terminal reported "vscode_terminal", both in the same field as the assistant name, so a run that never had an assistant to detect was indistinguishable from one whose assistant we failed to recognize. Together those two values were the majority of what the field reported. detect_coding_agent now answers only which assistant, returning "unknown" when no marker matches. detect_runtime_context answers where the process runs: ci, serverless, hosted_ide, notebook, container, the editor terminals, and the interactive/non_interactive fallback. Both ride on every span, so an assistant running inside CI reports both rather than one masking the other. The runtime markers are published platform contracts - CI providers, container and serverless runtimes, hosted IDEs - so unlike the assistant table they need no per-tool verification step. Presence is checked; no value is read. The assistant table is unchanged: its entries still require a confirmed, session-scoped variable, and the existing guard test still enforces that. Spans also carry project_id when the project declares one. It is read through the read-only accessor, since minting an id belongs to the CLI commands a user invoked rather than to a library call during execution, and it is omitted entirely for projects without one. The attributes are computed once per process and memoized, so the project file is not re-read for each provider. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: document execution environment telemetry attributes Adds the execution-environment row to the data table in en, ar, ko and pt-BR. Covers the assistant and runtime fields this branch splits apart and the project id, and states that detection reads only whether known environment variables are set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat(telemetry): widen assistant detection from published marker sets The table previously covered three assistants because the rest were unverified. They are documented after all: vercel/detect-agent publishes a machine-readable detection matrix (agents.json), corroborated by the proposal in agentsmd/agents.md#136 and by microsoft/vscode#311734. Adds cline, gemini_cli, augment, opencode, antigravity and junie, plus CLAUDE_CODE alongside CLAUDECODE. Gemini's marker is confirmed by its own docs, which state that run_shell_command sets GEMINI_CLI=1 in the subprocess environment. Rule 2 excluded several entries those sources list. Goose's GOOSE_PROVIDER and Copilot's COPILOT_MODEL and COPILOT_GITHUB_TOKEN are user configuration, and a committed .env carrying one would relabel every ordinary run - the AIDER_MODEL trap the guard test already pins, now parametrized over all four. Replit's REPL_ID names a hosted environment rather than an assistant, so it stays a runtime context. Copilot sets no session marker at all today; that is an open request upstream. The new assistants are ordered ahead of Cursor, since CURSOR_* is set for every integrated terminal and would otherwise mask anything spawned inside it - the same ordering Codex already needed. Also adds the proposed cross-vendor AI_AGENT marker as a last resort, reported as "other". It establishes that an assistant is present without naming one, and its value is an arbitrary vendor string, so the value is never read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(deps): raise gitpython and pypdf floors for new advisories gitpython 3.1.57 carries 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. Fixed in 3.1.58. pypdf 6.14.2 carries GHSA-fwg2-594c-jp42 and GHSA-fp3f-mc75-235c, unbounded runtime and memory on large content and /ToUnicode streams. Fixed in 6.15.0. Both floors were already pinned, so only the versions move. Their exclude-newer-package cutoffs had to move with them - 3.1.58 landed 2026-08-04 and 6.15.0 on 2026-08-06, both past the existing dates, so the resolver could not have seen either release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): share assistant precedence with the env-context path Four findings from the Cursor and CodeRabbit reviews, three of them the same root cause. get_env_context restated the precedence the shared table already defines, so every marker added for telemetry was invisible to it: a session exposing only CLAUDE_CODE reported claude_code on spans while emitting DefaultEnvEvent, and an assistant running inside a Cursor terminal reported that assistant on spans while emitting CursorEnvEvent. It now walks CODING_AGENT_ENV_MARKERS and maps the three assistants that have an event class of their own, defaulting the rest to DefaultEnvEvent. A test now asserts the two paths agree for every marker in the table, so they cannot drift again. The generic AI_AGENT marker was documented as presence-only but ran through the truthiness loop with everything else, so an empty value fell through to unknown. It moves out of the table and is checked by presence after it, which also keeps the named markers' truthiness intact. Azure Functions run on the App Service host and inherit WEBSITE_INSTANCE_ID, so moving that marker to paas would have relabelled them. The FUNCTIONS_* markers are checked first to keep them serverless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(telemetry): stop export assertions depending on test order test_all_common_attributes_land_on_exported_spans failed in CI with an IndexError on an empty span list, and only in one shard: the suite runs with OTEL_SDK_DISABLED set, so TracerProvider hands out no-op tracers and an export-based assertion sees zero spans rather than a wrong attribute. It passed only when it happened to run after a test whose fixture flips the variable, which random ordering decides. Adds an otel_enabled fixture that sets the variable for the four tests asserting on exported spans. Three of them predate this branch and had the same latent dependency - they are fixed here because the new test made the ordering hit reachable, and leaving them would keep the required check red. Verified by running every test in the file individually, all of which previously exposed the dependency, and the telemetry suite three times under random ordering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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'. لا بيانات شخصية. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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'. 개인 정보 없음. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
114
uv.lock
generated
114
uv.lock
generated
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user