mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
feat: add project_id to link OSS usage to an enterprise account (#6791)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (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
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (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
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
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* feat: add project_id to link OSS usage to an enterprise account Adds a stable per-project identifier so a project's OSS traces and runs can be attributed to an account after signup. There was no such identifier before: [tool.crewai] held only `type`, the deploy UUID was printed to the console but never persisted, Settings.org_uuid is global rather than per-project, and trace batches carried only crew_fingerprint/crew_name. The id lives in the project's pyproject.toml, so it is committed with the repository and stays stable across machines, teammates, CI, and containers - unlike a machine- or user-derived identifier, which is unstable in exactly the containerized production environments that matter most. crewai-core: - get_project_id(): read-only lookup of [tool.crewai].project_id. Safe for library code; never creates or modifies anything. - get_or_create_project_id(): mints a uuid4 and persists it, returning (id, created) so callers can tell the user. Best-effort - returns (None, False) for a missing, malformed, or read-only pyproject.toml rather than raising. - Insertion edits the raw TOML text instead of round-tripping through a writer, so comments, key order, and formatting elsewhere survive. The key is placed at the end of the [tool.crewai] table, before the next table header, so it cannot land in a neighbouring section. - LoginPayload and TraceExecutionContext gain optional project_id. Sent on two paths: - Traces: project_id is added to execution_context, which is sent on both the ephemeral and authenticated paths, so a project's traces remain attributable before and after the user creates an account. - Login: `crewai login` already sends the pseudonymous user_identifier on an authenticated request; adding project_id means one request carries account + user + project, which is the link itself. Minting is restricted to CLI commands the user explicitly invoked - `crewai create` for new projects and `crewai run` to backfill existing ones - and is announced when it happens. Library code only ever reads. Silently rewriting a user's pyproject.toml during Crew.kickoff() would be surprising. Privacy: project_id is a random uuid4 in a file the user commits. It is visible in a diff, contains nothing personal, and identifies a project rather than a person - so this needs none of the notice changes that attaching a user identifier to all telemetry would require. Tests: 18 new tests covering minting, stability, table placement, comment and formatting preservation, five pyproject layouts, the neighbouring-table regression, and graceful handling of missing/malformed/read-only files. Verified end-to-end that both create paths mint distinct ids, that the trace payload carries project_id on both the ephemeral and authenticated paths, and that the login payload carries user_identifier and project_id together. Follow-ups, deliberately not included: adding project_id to telemetry spans, and backend persistence of the (account, user_identifier, project_id) triple. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * refactor: drop the console announcement when minting project_id Minting now happens silently. With no message to print, the (id, created) tuple had no consumer, so simplify the API rather than keep the flag around for a hypothetical caller: - get_or_create_project_id() returns `str | None` instead of `tuple[str | None, bool]`. - Remove crewai_cli.utils.ensure_project_id, which existed only to print the message and discard the flag. The four call sites (crewai create crew, crewai create flow, crewai run, and tool-repository login) now call get_or_create_project_id directly. - Update tests for the simplified signature; still 18 tests covering minting, stability, table placement, formatting preservation, five pyproject layouts, and missing/malformed/read-only handling. Behaviour is otherwise unchanged: minting stays restricted to CLI commands the user invoked, library code still only reads via get_project_id, and a missing or read-only pyproject.toml still returns None rather than raising. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * fix: harden project_id minting against TOML corruption; address review Several reviewers found ways the raw-text edit could produce invalid TOML. Each is now fixed and covered by a test that fails without the fix. Duplicate project_id key (Cursor bugbot, Copilot x2): - get_project_id() reports a blank or non-string value as "absent", so a file containing `project_id = ""` took the insert path and gained a second project_id line - a duplicate key, and therefore invalid TOML that no tomli-based tool could read afterwards. - _insert_project_id is now _set_project_id: it replaces an existing assignment inside [tool.crewai] instead of appending unconditionally. Table header with a trailing comment (CodeRabbit major, Cursor bugbot): - `[tool.crewai] # config` is valid TOML but failed exact string equality, so the fallback appended a second [tool.crewai] header - a redefined table, also invalid TOML, and silent because get_project_id swallows the resulting decode error. - Added _is_table_header(), which tolerates a trailing comment and does not match similar names such as [tool.crewai-extra]. Writing into malformed TOML (Cursor bugbot, Copilot): - get_or_create_project_id relied on get_project_id, which cannot distinguish "no id" from "unparsable file", so it appended to files it could not parse. - The locked path now parses explicitly and bails on a decode error, and re-parses the updated content before writing, so this feature can never be the reason a project's pyproject.toml stops parsing. Concurrency and atomicity (CodeRabbit major): - Two CLI processes could both see no id, mint different uuids, and clobber each other, leaving a caller holding an id that is not on disk. Minting now takes the existing crewai_core cross-process lock, re-reads under it, and returns the id that persists. - Writes go through a temp file in the same directory plus os.replace, so an interruption cannot truncate pyproject.toml. File mode is copied across, and the temp file is removed on failure. - os.replace only needs a writable directory, which would have let an atomic write silently overwrite a file the user marked read-only; writability is now checked explicitly so that case still returns None. Line endings (CodeRabbit): - Path.read_text/write_text normalized CRLF to LF, so minting would rewrite a CRLF-committed file entirely. Read and write now use newline="" and the inserted line ending is derived from the existing content. Default create path skipped minting (Cursor bugbot): - `crewai create crew` defaults to create_json_crew; only the --classic and flow paths minted, so most new projects had no id until a later command. Wired into create_json_crew as well. Verified all three paths now mint distinct ids. Do not mint during login (CodeRabbit major): - ToolCommand.login ran get_or_create_project_id, which is outside the sanctioned minting commands and is invoked by `crewai tools create` from a freshly scaffolded directory before the project is persisted. It now uses the read-only get_project_id. Verified login leaves pyproject.toml untouched. Not applied: Copilot asked for a console message when an id is written, in create_crew and create_flow. Minting was made deliberately silent in the previous commit, so the (id, created) tuple and the announcement are both gone by design. Tests: 32 in test_project_id.py, up from 18. New cases cover blank and non-string existing ids, three commented-header forms, similar table names, malformed input, CRLF and LF preservation, concurrent minting convergence, file-mode preservation, and temp-file cleanup. Confirmed the header and duplicate-key tests fail when the fixes are reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * fix: never create [tool.crewai], treat whitespace ids as absent, harden test `crewai run` could rewrite unrelated projects (Cursor bugbot, high): - get_or_create_project_id ran before the cwd was established as a CrewAI project, and _set_project_id appended a [tool.crewai] table when none existed. Any directory with a pyproject.toml could therefore gain one - including on `crewai run --definition`, which may otherwise succeed. - _set_project_id no longer creates the table; it returns None when [tool.crewai] is absent, so a key is only ever added to a table the project already declares. The templates all ship the table, so no create path needs the old fallback. - The minting call in run_crew moved after the --definition early return, so an explicit-flow run does not touch the cwd at all. - Presence is checked, not truthiness: an empty [tool.crewai] is still a CrewAI marker, and get_crewai_project_config returns {} both for that and for an absent table. - Verified an unrelated project's pyproject.toml is byte-identical after a mint attempt. Whitespace-only project_id accepted as valid (CodeRabbit): - `project_id = " "` is truthy, so it was returned as an identity and would have propagated into login payloads and tracing context. It also meant the '" "' parameter of the replacement test asserted nothing. - Added _usable_project_id, which strips before deciding, used by both get_project_id and the locked mint path. Concurrency test could hang CI (CodeRabbit, major): - Neither the barrier nor the joins had timeouts, so a thread dying early or blocking on the lock would hang the job rather than fail it. The result count was also unchecked, so a dead thread still passed. - Added timeouts, an explicit liveness assertion, a result-count assertion, a lock around the shared result list, and corrected the docstring: this covers the read-modify-write race with threads, not the cross-process backend. Tests: 35, up from 32. New coverage for the absent-table refusal and three whitespace forms; the blank-id replacement case now asserts a real uuid replaced the blank value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * chore(deps): force gitpython 3.1.57+ for GHSA-p538-c434-8v24 and GHSA-3f7w-8rr8-f37f Unrelated to project_id; bundled here only because it blocks this PR's vulnerability scan. Two advisories were published for gitpython 3.1.55 after main last passed the scan: - GHSA-p538-c434-8v24: arbitrary file truncation via `git rev-list --output` argument injection. Fixed in 3.1.56. - GHSA-3f7w-8rr8-f37f: unguarded git option forwarding in IndexFile.checkout() and TagReference. Fixed in 3.1.57. - Bump the override floor to gitpython>=3.1.57 and declare the same floor in crewai-tools, so consumers installing the published package are covered and not only this repo's lock. - 3.1.57 was published 2026-07-26, past gitpython's exclude-newer-package cutoff of 2026-07-24, so that cutoff moves to 2026-07-27. Without it the floor is unresolvable. pip-audit against the updated lock reports no known vulnerabilities. Verified gitpython 3.1.57 resolves and that crewai_tools and crewai_cli.git still import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ from crewai_cli.provider import (
|
||||
)
|
||||
from crewai_cli.utils import (
|
||||
copy_template,
|
||||
get_or_create_project_id,
|
||||
is_dmn_mode_enabled,
|
||||
load_env_vars,
|
||||
write_env_file,
|
||||
@@ -320,6 +321,8 @@ def create_crew(
|
||||
copy_template(src_file, dst_file, name, class_name, folder_name)
|
||||
|
||||
if not parent_folder:
|
||||
# Minted at creation so the project has a stable identity from run one.
|
||||
get_or_create_project_id(folder_path / "pyproject.toml")
|
||||
initialize_if_git_available(folder_path)
|
||||
|
||||
click.secho(f"Crew {name} created successfully!", fg="green", bold=True)
|
||||
|
||||
@@ -5,6 +5,7 @@ import click
|
||||
from crewai_core.telemetry import Telemetry
|
||||
|
||||
from crewai_cli.git import initialize_if_git_available
|
||||
from crewai_cli.utils import get_or_create_project_id
|
||||
from crewai_cli.version import get_crewai_tools_dependency
|
||||
|
||||
|
||||
@@ -31,6 +32,8 @@ def create_flow(name: str, *, declarative: bool = False) -> None:
|
||||
else:
|
||||
_create_python_flow(name, class_name, folder_name, project_root)
|
||||
|
||||
# Minted at creation so the project has a stable identity from run one.
|
||||
get_or_create_project_id(project_root / "pyproject.toml")
|
||||
initialize_if_git_available(project_root)
|
||||
|
||||
click.secho(f"Flow {name} created successfully!", fg="green", bold=True)
|
||||
|
||||
@@ -18,6 +18,7 @@ from crewai_cli.model_catalog import get_provider_models
|
||||
from crewai_cli.tui_picker import pick_many, pick_one
|
||||
from crewai_cli.utils import (
|
||||
enable_prompt_line_editing,
|
||||
get_or_create_project_id,
|
||||
is_dmn_mode_enabled,
|
||||
load_env_vars,
|
||||
render_template,
|
||||
@@ -968,6 +969,9 @@ def create_json_crew(
|
||||
for model in models:
|
||||
_setup_env(folder_path, model)
|
||||
|
||||
# Minted at creation so the project has a stable identity from run one.
|
||||
# This is the default `crewai create crew` path, not just --classic.
|
||||
get_or_create_project_id(folder_path / "pyproject.toml")
|
||||
initialize_if_git_available(folder_path)
|
||||
|
||||
click.echo()
|
||||
|
||||
@@ -20,6 +20,7 @@ from crewai_cli.input_prompt import (
|
||||
)
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
get_or_create_project_id,
|
||||
is_dmn_mode_enabled,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
|
||||
@@ -627,6 +628,15 @@ def run_crew(
|
||||
return
|
||||
|
||||
pyproject_data = read_toml()
|
||||
|
||||
# Backfills projects created before project_id existed. Only here, in a
|
||||
# command the user explicitly invoked - never from the SDK during kickoff.
|
||||
# Placed after the --definition early return so an explicit-flow run does
|
||||
# not touch the cwd; get_or_create_project_id itself refuses to act unless
|
||||
# [tool.crewai] is already present, so an unrelated project is never
|
||||
# rewritten.
|
||||
get_or_create_project_id()
|
||||
|
||||
if json_crew_definition := configured_project_json_crew(pyproject_data):
|
||||
# Declarative (JSON) crews resolve inputs the same way flows do: --inputs
|
||||
# layers over the crew's declared defaults, missing {placeholder}s are
|
||||
|
||||
@@ -17,6 +17,7 @@ from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_tool_repository_credentials,
|
||||
get_project_description,
|
||||
get_project_id,
|
||||
get_project_name,
|
||||
get_project_version,
|
||||
read_toml,
|
||||
@@ -228,8 +229,12 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
|
||||
def login(self) -> None:
|
||||
get_user_id = _require_get_user_id()
|
||||
# Read-only: login is not one of the sanctioned minting commands, and
|
||||
# `crewai tools create` calls it from inside a freshly scaffolded
|
||||
# directory before the tool project is persisted.
|
||||
login_response = self.plus_api_client.login_to_tool_repository(
|
||||
user_identifier=get_user_id()
|
||||
user_identifier=get_user_id(),
|
||||
project_id=get_project_id(),
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
|
||||
@@ -9,7 +9,9 @@ from typing import Any
|
||||
|
||||
import click
|
||||
from crewai_core.project import (
|
||||
get_or_create_project_id as get_or_create_project_id,
|
||||
get_project_description as get_project_description,
|
||||
get_project_id as get_project_id,
|
||||
get_project_name as get_project_name,
|
||||
get_project_version as get_project_version,
|
||||
parse_toml as parse_toml,
|
||||
@@ -30,7 +32,9 @@ __all__ = [
|
||||
"copy_template",
|
||||
"enable_prompt_line_editing",
|
||||
"fetch_and_json_env_file",
|
||||
"get_or_create_project_id",
|
||||
"get_project_description",
|
||||
"get_project_id",
|
||||
"get_project_name",
|
||||
"get_project_version",
|
||||
"is_dmn_mode_enabled",
|
||||
|
||||
@@ -69,7 +69,7 @@ class _WithUserIdentifier(TypedDict):
|
||||
|
||||
|
||||
class LoginPayload(_WithUserIdentifier):
|
||||
pass
|
||||
project_id: NotRequired[str]
|
||||
|
||||
|
||||
class TraceExecutionContext(TypedDict):
|
||||
@@ -78,6 +78,7 @@ class TraceExecutionContext(TypedDict):
|
||||
flow_name: str | None
|
||||
crewai_version: str
|
||||
privacy_level: str
|
||||
project_id: NotRequired[str | None]
|
||||
|
||||
|
||||
class TraceExecutionMetadata(TypedDict):
|
||||
@@ -229,11 +230,24 @@ class PlusAPI:
|
||||
return client.request(method, url, files=files, **request_kwargs)
|
||||
|
||||
def login_to_tool_repository(
|
||||
self, user_identifier: str | None = None
|
||||
self, user_identifier: str | None = None, project_id: str | None = None
|
||||
) -> httpx.Response:
|
||||
"""Log in to the tool repository.
|
||||
|
||||
This request is authenticated, so sending user_identifier and project_id
|
||||
alongside it links the account to the local pseudonymous user id and to
|
||||
the project the command was run from - letting prior anonymous usage of
|
||||
that project be attributed after signup.
|
||||
|
||||
Args:
|
||||
user_identifier: Local pseudonymous user id.
|
||||
project_id: ``[tool.crewai].project_id`` of the current project.
|
||||
"""
|
||||
payload: LoginPayload = {}
|
||||
if user_identifier:
|
||||
payload["user_identifier"] = user_identifier
|
||||
if project_id:
|
||||
payload["project_id"] = project_id
|
||||
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload)
|
||||
|
||||
def get_tool(self, handle: str) -> httpx.Response:
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
import os
|
||||
from pathlib import Path, PureWindowsPath
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from rich.console import Console
|
||||
import tomli
|
||||
|
||||
from crewai_core.lock_store import lock as store_lock
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
@@ -221,3 +227,281 @@ def get_project_description(
|
||||
return _get_project_attribute(
|
||||
pyproject_path, ["project", "description"], require=require
|
||||
)
|
||||
|
||||
|
||||
_PROJECT_ID_KEY = "project_id"
|
||||
|
||||
|
||||
def get_project_id(pyproject_path: str | Path = "pyproject.toml") -> str | None:
|
||||
"""Return ``[tool.crewai].project_id`` if the project has one.
|
||||
|
||||
Read-only and safe to call from library code: it never creates or modifies
|
||||
anything. Use this everywhere except the CLI commands that are allowed to
|
||||
mint an id (see :func:`get_or_create_project_id`).
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the project's ``pyproject.toml``.
|
||||
|
||||
Returns:
|
||||
The project id, or None when the file is missing, unreadable, or has
|
||||
no id configured.
|
||||
"""
|
||||
try:
|
||||
pyproject_data = read_toml(pyproject_path)
|
||||
except (OSError, tomli.TOMLDecodeError):
|
||||
return None
|
||||
|
||||
return _usable_project_id(get_crewai_project_config(pyproject_data))
|
||||
|
||||
|
||||
def _has_crewai_table(pyproject_data: dict[str, Any]) -> bool:
|
||||
"""True if ``[tool.crewai]`` exists, even when empty.
|
||||
|
||||
Distinguishes "declared but empty" from "absent", which
|
||||
:func:`get_crewai_project_config` cannot: it returns ``{}`` for both.
|
||||
"""
|
||||
tool_config = pyproject_data.get("tool")
|
||||
return isinstance(tool_config, dict) and isinstance(tool_config.get("crewai"), dict)
|
||||
|
||||
|
||||
def _usable_project_id(crewai_config: dict[str, Any]) -> str | None:
|
||||
"""Return the configured id if it is usable as an identifier.
|
||||
|
||||
Whitespace-only values are treated as absent: they are truthy in Python but
|
||||
are not an identity, and would otherwise propagate into login payloads and
|
||||
tracing context.
|
||||
"""
|
||||
project_id = crewai_config.get(_PROJECT_ID_KEY)
|
||||
if not isinstance(project_id, str):
|
||||
return None
|
||||
stripped = project_id.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
def get_or_create_project_id(
|
||||
pyproject_path: str | Path = "pyproject.toml",
|
||||
) -> str | None:
|
||||
"""Return the project's id, minting and persisting one if absent.
|
||||
|
||||
Writes ``project_id`` into the ``[tool.crewai]`` table so it is committed
|
||||
with the repository. That makes it stable across machines, teammates, CI,
|
||||
and containers - unlike a machine- or user-derived identifier.
|
||||
|
||||
Only CLI commands the user explicitly invoked should call this. Library
|
||||
code must use :func:`get_project_id` instead; silently rewriting a user's
|
||||
``pyproject.toml`` during ``Crew.kickoff()`` would be surprising.
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the project's ``pyproject.toml``.
|
||||
|
||||
Returns:
|
||||
The project id, or None when ``pyproject.toml`` is missing, malformed,
|
||||
or not writable. Best-effort - never raises.
|
||||
"""
|
||||
path = Path(pyproject_path)
|
||||
if not path.is_file():
|
||||
return None
|
||||
|
||||
# Cross-process lock: two CLI invocations could otherwise both see no id,
|
||||
# mint different uuids, and clobber each other - leaving one caller holding
|
||||
# an id that is not the one on disk.
|
||||
try:
|
||||
with store_lock(_project_id_lock_name(path)):
|
||||
return _get_or_create_project_id_locked(path)
|
||||
except Exception:
|
||||
# Lock backend unavailable; a torn write is worse than no id.
|
||||
return get_project_id(path)
|
||||
|
||||
|
||||
def _project_id_lock_name(path: Path) -> str:
|
||||
"""Return a stable lock name for a project's ``pyproject.toml``."""
|
||||
return f"file:{os.path.realpath(path)}"
|
||||
|
||||
|
||||
def _get_or_create_project_id_locked(path: Path) -> str | None:
|
||||
"""Read-modify-write the project id while holding the lock.
|
||||
|
||||
Re-reads under the lock so a concurrent minter's id is returned rather than
|
||||
overwritten.
|
||||
"""
|
||||
try:
|
||||
content = _read_preserving_newlines(path)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
# Parse here rather than relying on get_project_id, which reports malformed
|
||||
# files and absent ids identically. Appending to a file we cannot parse
|
||||
# would corrupt it further, so bail instead.
|
||||
try:
|
||||
pyproject_data = parse_toml(content)
|
||||
except (tomli.TOMLDecodeError, ValueError):
|
||||
return None
|
||||
|
||||
crewai_config = get_crewai_project_config(pyproject_data)
|
||||
existing = _usable_project_id(crewai_config)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
# Only ever add a key to an existing [tool.crewai] table. Creating the table
|
||||
# would rewrite the pyproject.toml of any directory that merely happens to
|
||||
# have one, which `crewai run` could otherwise do before it has established
|
||||
# that the cwd is a CrewAI project at all.
|
||||
#
|
||||
# Presence, not truthiness: an empty `[tool.crewai]` table is still a CrewAI
|
||||
# marker, and get_crewai_project_config returns {} for both cases.
|
||||
if not _has_crewai_table(pyproject_data):
|
||||
return None
|
||||
|
||||
project_id = str(uuid.uuid4())
|
||||
updated = _set_project_id(content, project_id)
|
||||
if updated is None:
|
||||
return None
|
||||
|
||||
# Verify before writing: never leave a project with unparsable TOML because
|
||||
# of this feature.
|
||||
try:
|
||||
parse_toml(updated)
|
||||
except (tomli.TOMLDecodeError, ValueError):
|
||||
return None
|
||||
|
||||
# Checked explicitly: os.replace only needs a writable *directory*, so an
|
||||
# atomic write would happily overwrite a file the user marked read-only.
|
||||
if not os.access(path, os.W_OK):
|
||||
return None
|
||||
|
||||
try:
|
||||
_write_atomically(path, updated)
|
||||
except OSError:
|
||||
# Read-only checkout, permissions, container FS - not worth failing over.
|
||||
return None
|
||||
|
||||
return project_id
|
||||
|
||||
|
||||
def _read_preserving_newlines(path: Path) -> str:
|
||||
"""Read text without translating line endings.
|
||||
|
||||
``Path.read_text`` normalizes CRLF to LF, so a later write would silently
|
||||
convert a CRLF-committed file to LF and show up as a whole-file diff.
|
||||
"""
|
||||
with path.open("r", encoding="utf-8", newline="") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
def _write_atomically(path: Path, content: str) -> None:
|
||||
"""Replace ``path`` with ``content`` via a temp file in the same directory.
|
||||
|
||||
An interrupted or concurrent write must never leave a truncated
|
||||
``pyproject.toml`` behind.
|
||||
"""
|
||||
directory = path.parent
|
||||
handle = tempfile.NamedTemporaryFile(
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
newline="",
|
||||
dir=directory,
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
)
|
||||
tmp_path = Path(handle.name)
|
||||
try:
|
||||
with handle:
|
||||
handle.write(content)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
shutil.copymode(path, tmp_path)
|
||||
os.replace(tmp_path, path)
|
||||
except BaseException:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def _is_table_header(line: str, table: str) -> bool:
|
||||
"""True if ``line`` opens ``table``, tolerating a trailing inline comment.
|
||||
|
||||
``[tool.crewai] # config`` is valid TOML. Comparing the stripped line to
|
||||
the header verbatim would miss it, and the caller would then append a second
|
||||
``[tool.crewai]`` header - a duplicate table definition, which is invalid
|
||||
TOML.
|
||||
"""
|
||||
stripped = line.strip()
|
||||
if not stripped.startswith("["):
|
||||
return False
|
||||
closing = stripped.find("]")
|
||||
if closing == -1:
|
||||
return False
|
||||
if stripped[: closing + 1] != table:
|
||||
return False
|
||||
remainder = stripped[closing + 1 :].strip()
|
||||
return remainder == "" or remainder.startswith("#")
|
||||
|
||||
|
||||
def _is_any_table_header(line: str) -> bool:
|
||||
"""True if ``line`` opens any TOML table or array-of-tables."""
|
||||
return line.lstrip().startswith("[")
|
||||
|
||||
|
||||
def _project_id_key_index(lines: list[str], start: int, end: int) -> int | None:
|
||||
"""Return the index of an existing ``project_id`` assignment in a range."""
|
||||
for index in range(start, end):
|
||||
candidate = lines[index].strip()
|
||||
if not candidate or candidate.startswith("#"):
|
||||
continue
|
||||
key, separator, _ = candidate.partition("=")
|
||||
if separator and key.strip().strip("\"'") == _PROJECT_ID_KEY:
|
||||
return index
|
||||
return None
|
||||
|
||||
|
||||
def _set_project_id(content: str, project_id: str) -> str | None:
|
||||
"""Set ``project_id`` in the ``[tool.crewai]`` table of TOML source text.
|
||||
|
||||
Replaces an existing ``project_id`` assignment rather than adding a second
|
||||
one: a blank or non-string value reads as "absent", and appending in that
|
||||
case would produce a duplicate key and therefore invalid TOML.
|
||||
|
||||
Edits the raw text rather than round-tripping through a TOML writer so
|
||||
formatting, ordering, and comments in the rest of the file are preserved.
|
||||
|
||||
Args:
|
||||
content: Full contents of a ``pyproject.toml``.
|
||||
project_id: The id to set.
|
||||
|
||||
Returns:
|
||||
Updated file contents, or None if the edit could not be made safely.
|
||||
"""
|
||||
lines = content.splitlines(keepends=True)
|
||||
newline = "\r\n" if "\r\n" in content else "\n"
|
||||
entry = f'{_PROJECT_ID_KEY} = "{project_id}"{newline}'
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if not _is_table_header(line, "[tool.crewai]"):
|
||||
continue
|
||||
|
||||
# Bound the table: everything up to the next table header.
|
||||
table_end = len(lines)
|
||||
for offset in range(index + 1, len(lines)):
|
||||
if _is_any_table_header(lines[offset]):
|
||||
table_end = offset
|
||||
break
|
||||
|
||||
existing = _project_id_key_index(lines, index + 1, table_end)
|
||||
if existing is not None:
|
||||
lines[existing] = entry
|
||||
return "".join(lines)
|
||||
|
||||
# Step back over trailing blank lines so the key stays in the table.
|
||||
insert_at = table_end
|
||||
while insert_at > index + 1 and not lines[insert_at - 1].strip():
|
||||
insert_at -= 1
|
||||
|
||||
if insert_at > 0 and not lines[insert_at - 1].endswith(("\n", "\r")):
|
||||
lines[insert_at - 1] += newline
|
||||
|
||||
lines.insert(insert_at, entry)
|
||||
return "".join(lines)
|
||||
|
||||
# No [tool.crewai] table. Never create one: that would let this feature
|
||||
# rewrite the pyproject.toml of a directory that is not a CrewAI project.
|
||||
return None
|
||||
|
||||
@@ -107,7 +107,9 @@ stagehand = [
|
||||
"stagehand>=0.4.1",
|
||||
]
|
||||
github = [
|
||||
"gitpython>=3.1.55,<4",
|
||||
# <3.1.57 has GHSA-p538-c434-8v24 (arbitrary file truncation) and
|
||||
# GHSA-3f7w-8rr8-f37f (unguarded git option forwarding).
|
||||
"gitpython>=3.1.57,<4",
|
||||
"PyGithub==1.59.1",
|
||||
]
|
||||
rag = [
|
||||
|
||||
@@ -14,6 +14,7 @@ from crewai_core.plus_api import (
|
||||
TraceExecutionMetadata,
|
||||
TraceFinalizePayload,
|
||||
)
|
||||
from crewai_core.project import get_project_id
|
||||
from crewai_core.settings import Settings
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -145,6 +146,10 @@ class TraceBatchManager:
|
||||
"flow_name": execution_metadata.get("flow_name", None),
|
||||
"crewai_version": self.current_batch.version,
|
||||
"privacy_level": user_context.get("privacy_level", "standard"),
|
||||
# Read-only: never mints an id. Sent on both the ephemeral and
|
||||
# authenticated paths, so a project's traces stay attributable
|
||||
# to it before and after the user creates an account.
|
||||
"project_id": get_project_id(),
|
||||
}
|
||||
execution_metadata_payload: TraceExecutionMetadata = {
|
||||
"expected_duration_estimate": execution_metadata.get(
|
||||
|
||||
342
lib/crewai/tests/telemetry/test_project_id.py
Normal file
342
lib/crewai/tests/telemetry/test_project_id.py
Normal file
@@ -0,0 +1,342 @@
|
||||
"""Tests for the project_id used to link OSS usage to an enterprise account."""
|
||||
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_core.project import (
|
||||
get_or_create_project_id,
|
||||
get_project_id,
|
||||
parse_toml,
|
||||
)
|
||||
|
||||
|
||||
CREW_PYPROJECT = """\
|
||||
[project]
|
||||
name = "my_crew"
|
||||
version = "0.1.0"
|
||||
dependencies = ["crewai"]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyproject(tmp_path):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(CREW_PYPROJECT)
|
||||
return path
|
||||
|
||||
|
||||
def test_returns_none_when_no_id_configured(pyproject):
|
||||
assert get_project_id(pyproject) is None
|
||||
|
||||
|
||||
def test_mints_and_persists_an_id(pyproject):
|
||||
project_id = get_or_create_project_id(pyproject)
|
||||
|
||||
assert uuid.UUID(project_id)
|
||||
assert get_project_id(pyproject) == project_id
|
||||
|
||||
|
||||
def test_id_is_stable_across_calls(pyproject):
|
||||
first = get_or_create_project_id(pyproject)
|
||||
second = get_or_create_project_id(pyproject)
|
||||
|
||||
assert first == second, "must not mint a second id"
|
||||
assert uuid.UUID(first)
|
||||
|
||||
|
||||
def test_id_lands_in_the_tool_crewai_table(pyproject):
|
||||
project_id = get_or_create_project_id(pyproject)
|
||||
|
||||
data = parse_toml(pyproject.read_text())
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id
|
||||
assert data["tool"]["crewai"]["type"] == "crew", "existing keys must survive"
|
||||
|
||||
|
||||
def test_other_tables_are_preserved(pyproject):
|
||||
get_or_create_project_id(pyproject)
|
||||
|
||||
data = parse_toml(pyproject.read_text())
|
||||
assert data["project"]["name"] == "my_crew"
|
||||
assert data["project"]["dependencies"] == ["crewai"]
|
||||
assert data["build-system"]["build-backend"] == "hatchling.build"
|
||||
|
||||
|
||||
def test_comments_and_formatting_are_preserved(tmp_path):
|
||||
"""Raw-text editing rather than a TOML round-trip, so comments survive."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(
|
||||
'# top comment\n[project]\nname = "x" # inline comment\n\n[tool.crewai]\ntype = "flow"\n'
|
||||
)
|
||||
|
||||
get_or_create_project_id(path)
|
||||
|
||||
content = path.read_text()
|
||||
assert "# top comment" in content
|
||||
assert "# inline comment" in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("source", "label"),
|
||||
[
|
||||
('[project]\nname = "x"\n\n[tool.crewai]\ntype = "crew"\n', "table then EOF"),
|
||||
('[tool.crewai]\ntype = "crew"', "no trailing newline"),
|
||||
('[project]\nname = "x"\n[tool.crewai]\n[other]\na = 1\n', "empty table"),
|
||||
(
|
||||
'[tool.crewai]\ntype = "crew"\n\n\n[build-system]\nrequires = []\n',
|
||||
"blank lines before next table",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_produces_valid_toml_for_varied_layouts(tmp_path, source, label):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(source)
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
assert project_id is not None, label
|
||||
data = parse_toml(path.read_text())
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id, label
|
||||
|
||||
|
||||
def test_id_does_not_leak_into_a_neighbouring_table(tmp_path):
|
||||
"""The key must never land under [build-system]."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(
|
||||
'[tool.crewai]\ntype = "crew"\n\n[build-system]\nrequires = ["hatchling"]\n'
|
||||
)
|
||||
|
||||
get_or_create_project_id(path)
|
||||
|
||||
data = parse_toml(path.read_text())
|
||||
assert "project_id" in data["tool"]["crewai"]
|
||||
assert "project_id" not in data["build-system"]
|
||||
|
||||
|
||||
def test_absent_tool_crewai_table_is_never_created(tmp_path):
|
||||
"""Refuse to mint rather than rewrite a non-CrewAI project's pyproject.toml.
|
||||
|
||||
`crewai run` in any directory that merely happens to have a pyproject.toml
|
||||
must not gain a [tool.crewai] table as a side effect.
|
||||
"""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
original = '[project]\nname = "unrelated"\n'
|
||||
path.write_text(original)
|
||||
|
||||
assert get_or_create_project_id(path) is None
|
||||
assert path.read_text() == original, "unrelated project was modified"
|
||||
|
||||
|
||||
def test_missing_file_is_not_an_error(tmp_path):
|
||||
assert get_or_create_project_id(tmp_path / "nope.toml") is None
|
||||
|
||||
|
||||
def test_malformed_toml_is_not_an_error(tmp_path):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text("this is not [valid toml")
|
||||
|
||||
assert get_project_id(path) is None
|
||||
|
||||
|
||||
def test_read_only_file_is_not_an_error(pyproject):
|
||||
"""A read-only checkout must not break the command that called this."""
|
||||
pyproject.chmod(0o444)
|
||||
try:
|
||||
project_id = get_or_create_project_id(pyproject)
|
||||
finally:
|
||||
pyproject.chmod(0o644)
|
||||
|
||||
assert project_id is None
|
||||
|
||||
|
||||
def test_get_project_id_never_creates_anything(pyproject):
|
||||
"""Library code calls the read-only variant; it must not mutate the file."""
|
||||
before = pyproject.read_text()
|
||||
|
||||
assert get_project_id(pyproject) is None
|
||||
|
||||
assert pyproject.read_text() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ['""', "' '", '"\\t"'])
|
||||
def test_blank_or_whitespace_id_is_treated_as_absent(tmp_path, blank):
|
||||
"""Whitespace is truthy in Python but is not an identity.
|
||||
|
||||
Accepting it would propagate a useless value into login payloads and
|
||||
tracing context.
|
||||
"""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(f'[tool.crewai]\ntype = "crew"\nproject_id = {blank}\n')
|
||||
|
||||
assert get_project_id(path) is None
|
||||
|
||||
|
||||
def test_malformed_toml_is_never_written_to(tmp_path):
|
||||
"""Appending to a file we cannot parse would corrupt it further."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
original = 'this is not [valid toml\nproject_id = "x'
|
||||
path.write_text(original)
|
||||
|
||||
assert get_or_create_project_id(path) is None
|
||||
assert path.read_text() == original, "malformed file must be left untouched"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("blank", ['""', "''", '" "', '"\\t\\t"'])
|
||||
def test_blank_existing_id_is_replaced_not_duplicated(tmp_path, blank):
|
||||
"""A blank id reads as absent; appending would make a duplicate key."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(f'[tool.crewai]\ntype = "crew"\nproject_id = {blank}\n')
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
content = path.read_text()
|
||||
assert content.count("project_id") == 1, f"duplicate key: {content!r}"
|
||||
data = parse_toml(content) # would raise on a duplicate key
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id
|
||||
assert data["tool"]["crewai"]["type"] == "crew"
|
||||
assert uuid.UUID(project_id), "must mint a real id, not keep the blank one"
|
||||
|
||||
|
||||
def test_non_string_existing_id_is_replaced(tmp_path):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text("[tool.crewai]\nproject_id = 42\n")
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
data = parse_toml(path.read_text())
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id
|
||||
assert isinstance(project_id, str)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header",
|
||||
[
|
||||
"[tool.crewai] # crewai config",
|
||||
"[tool.crewai]# no space",
|
||||
"[tool.crewai]\t# tab then comment",
|
||||
],
|
||||
)
|
||||
def test_table_header_with_trailing_comment_is_found(tmp_path, header):
|
||||
"""A commented header is valid TOML; missing it appends a duplicate table."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(f'{header}\ntype = "crew"\n')
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
content = path.read_text()
|
||||
assert content.count("[tool.crewai]") == 1, f"duplicate table: {content!r}"
|
||||
data = parse_toml(content) # would raise on a redefined table
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id
|
||||
assert data["tool"]["crewai"]["type"] == "crew"
|
||||
|
||||
|
||||
def test_similar_table_names_are_not_matched(tmp_path):
|
||||
"""[tool.crewai-extra] must not be mistaken for [tool.crewai]."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text('[tool.crewai-extra]\nfoo = 1\n\n[tool.crewai]\ntype = "crew"\n')
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
data = parse_toml(path.read_text())
|
||||
assert data["tool"]["crewai"]["project_id"] == project_id
|
||||
assert "project_id" not in data["tool"]["crewai-extra"]
|
||||
|
||||
|
||||
def test_crlf_line_endings_are_preserved(tmp_path):
|
||||
"""read_text/write_text would silently rewrite the whole file as LF."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_bytes(b'[project]\r\nname = "x"\r\n\r\n[tool.crewai]\r\ntype = "crew"\r\n')
|
||||
|
||||
project_id = get_or_create_project_id(path)
|
||||
|
||||
raw = path.read_bytes()
|
||||
assert b"\r\n" in raw
|
||||
assert raw.count(b"\n") == raw.count(b"\r\n"), "mixed line endings introduced"
|
||||
assert parse_toml(raw.decode())["tool"]["crewai"]["project_id"] == project_id
|
||||
|
||||
|
||||
def test_lf_file_stays_lf(tmp_path):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_bytes(b'[tool.crewai]\ntype = "crew"\n')
|
||||
|
||||
get_or_create_project_id(path)
|
||||
|
||||
assert b"\r\n" not in path.read_bytes()
|
||||
|
||||
|
||||
def test_concurrent_minting_converges_on_one_id(tmp_path):
|
||||
"""Concurrent minters must all return the id that ends up on disk.
|
||||
|
||||
Uses threads in one process, so it covers the read-modify-write race rather
|
||||
than the cross-process lock backend itself.
|
||||
"""
|
||||
import threading
|
||||
|
||||
workers = 8
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(CREW_PYPROJECT)
|
||||
|
||||
returned: list[str | None] = []
|
||||
results_lock = threading.Lock()
|
||||
# Timed out rather than unbounded: a thread dying before the barrier, or
|
||||
# blocking on the lock, would otherwise hang CI instead of failing.
|
||||
start = threading.Barrier(workers, timeout=30)
|
||||
|
||||
def mint() -> None:
|
||||
start.wait()
|
||||
project_id = get_or_create_project_id(path)
|
||||
with results_lock:
|
||||
returned.append(project_id)
|
||||
|
||||
threads = [threading.Thread(target=mint) for _ in range(workers)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join(timeout=30)
|
||||
|
||||
assert not [t for t in threads if t.is_alive()], "thread did not finish in time"
|
||||
assert len(returned) == workers, f"only {len(returned)}/{workers} threads returned"
|
||||
|
||||
persisted = parse_toml(path.read_text())["tool"]["crewai"]["project_id"]
|
||||
assert set(returned) == {persisted}, (
|
||||
f"callers disagreed with disk: returned={set(returned)} persisted={persisted}"
|
||||
)
|
||||
|
||||
|
||||
def test_file_mode_is_preserved(tmp_path):
|
||||
"""The atomic replace must not widen permissions on pyproject.toml."""
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(CREW_PYPROJECT)
|
||||
path.chmod(0o600)
|
||||
|
||||
get_or_create_project_id(path)
|
||||
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
def test_no_temp_files_left_behind(tmp_path):
|
||||
path = tmp_path / "pyproject.toml"
|
||||
path.write_text(CREW_PYPROJECT)
|
||||
|
||||
get_or_create_project_id(path)
|
||||
|
||||
assert [p.name for p in tmp_path.iterdir()] == ["pyproject.toml"]
|
||||
|
||||
|
||||
def test_ids_are_unique_across_projects(tmp_path):
|
||||
ids = set()
|
||||
for name in ("a", "b", "c"):
|
||||
path = tmp_path / name / "pyproject.toml"
|
||||
path.parent.mkdir()
|
||||
path.write_text(CREW_PYPROJECT)
|
||||
project_id = get_or_create_project_id(path)
|
||||
ids.add(project_id)
|
||||
|
||||
assert len(ids) == 3
|
||||
@@ -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-24T00:00:00Z" }
|
||||
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" }
|
||||
|
||||
# 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.
|
||||
@@ -190,6 +190,9 @@ exclude-newer-package = { pypdf = "2026-06-24T00:00:00Z", msgpack = "2026-06-20T
|
||||
# gitpython <3.1.51 has GHSA-2f96-g7mh-g2hx, GHSA-v396-v7q4-x2qj, and GHSA-956x-8gvw-wg5v.
|
||||
# gitpython <=3.1.51 has GHSA-rwj8-pgh3-r573; fixed in 3.1.52.
|
||||
# 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.
|
||||
# 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)
|
||||
@@ -224,7 +227,7 @@ override-dependencies = [
|
||||
"pypdf>=6.14.2,<7",
|
||||
"uv>=0.11.15,<1",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"gitpython>=3.1.55,<4",
|
||||
"gitpython>=3.1.57,<4",
|
||||
"pyasn1>=0.6.4",
|
||||
"langsmith>=0.8.18,<1",
|
||||
"authlib>=1.6.12",
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@@ -19,7 +19,7 @@ exclude-newer-span = "P3D"
|
||||
[options.exclude-newer-package]
|
||||
msgpack = "2026-06-20T00:00:00Z"
|
||||
langsmith = "2026-06-20T00:00:00Z"
|
||||
gitpython = "2026-07-24T00:00:00Z"
|
||||
gitpython = "2026-07-27T00:00:00Z"
|
||||
pypdf = "2026-06-24T00:00:00Z"
|
||||
pydantic-settings = "2026-06-20T00:00:00Z"
|
||||
|
||||
@@ -37,7 +37,7 @@ overrides = [
|
||||
{ name = "authlib", specifier = ">=1.6.12" },
|
||||
{ name = "cryptography", specifier = ">=46.0.7" },
|
||||
{ name = "docling-core", extras = ["chunking"], specifier = ">=2.74.1" },
|
||||
{ name = "gitpython", specifier = ">=3.1.55,<4" },
|
||||
{ name = "gitpython", specifier = ">=3.1.57,<4" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3,<2" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2,<2" },
|
||||
{ name = "langsmith", specifier = ">=0.8.18,<1" },
|
||||
@@ -1758,7 +1758,7 @@ requires-dist = [
|
||||
{ name = "e2b-code-interpreter", marker = "extra == 'e2b'", specifier = "~=2.6.0" },
|
||||
{ name = "exa-py", marker = "extra == 'exa-py'", specifier = ">=1.8.7" },
|
||||
{ name = "firecrawl-py", marker = "extra == 'firecrawl-py'", specifier = ">=1.8.0" },
|
||||
{ name = "gitpython", marker = "extra == 'github'", specifier = ">=3.1.55,<4" },
|
||||
{ name = "gitpython", marker = "extra == 'github'", specifier = ">=3.1.57,<4" },
|
||||
{ name = "hyperbrowser", marker = "extra == 'hyperbrowser'", specifier = ">=0.18.0" },
|
||||
{ name = "langchain-apify", marker = "extra == 'apify'", specifier = ">=0.1.2,<1.0.0" },
|
||||
{ name = "linkup-sdk", marker = "extra == 'linkup-sdk'", specifier = ">=0.2.2" },
|
||||
@@ -2771,14 +2771,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gitpython"
|
||||
version = "3.1.55"
|
||||
version = "3.1.57"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "gitdb" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/ab/ba0d29f2fa2277ed6256b2ac09003494045355f3a10bf32f351761287870/gitpython-3.1.55.tar.gz", hash = "sha256:781e3b1624dad81b24e9524bf0297b69786a0706db2cbceec1e2b05c38e5152f", size = 225071, upload-time = "2026-07-23T02:52:43.246Z" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/6a/d3b8208d2f8aac66abe8ccc1c23fa2c89464ec42cc71a601e95d05902428/gitpython-3.1.55-py3-none-any.whl", hash = "sha256:7c9ec1e69c158c081632ab35c41471e302c96db2ae42165036a5d2403378812e", size = 216590, upload-time = "2026-07-23T02:52:41.932Z" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user