mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-11 00:42:48 +00:00
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
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)
|
||||
|
||||
@@ -20,6 +20,7 @@ from crewai_cli.input_prompt import (
|
||||
)
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_all_tool_credentials,
|
||||
ensure_project_id,
|
||||
is_dmn_mode_enabled,
|
||||
)
|
||||
from crewai_cli.version import get_crewai_tools_dependency, get_crewai_version
|
||||
@@ -617,6 +618,10 @@ def run_crew(
|
||||
or declarative (JSON) crew. Layered over the definition's own
|
||||
defaults; missing required values are prompted for interactively.
|
||||
"""
|
||||
# Backfills projects created before project_id existed. Only here, in a
|
||||
# command the user explicitly invoked - never from the SDK during kickoff.
|
||||
ensure_project_id()
|
||||
|
||||
# --definition is a pure override: run that flow directly.
|
||||
if definition is not None:
|
||||
_run_explicit_declarative_flow(
|
||||
|
||||
@@ -16,6 +16,7 @@ from crewai_cli.config import Settings
|
||||
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
from crewai_cli.utils import (
|
||||
build_env_with_tool_repository_credentials,
|
||||
ensure_project_id,
|
||||
get_project_description,
|
||||
get_project_name,
|
||||
get_project_version,
|
||||
@@ -229,7 +230,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
def login(self) -> None:
|
||||
get_user_id = _require_get_user_id()
|
||||
login_response = self.plus_api_client.login_to_tool_repository(
|
||||
user_identifier=get_user_id()
|
||||
user_identifier=get_user_id(),
|
||||
project_id=ensure_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,
|
||||
@@ -29,8 +31,11 @@ __all__ = [
|
||||
"build_env_with_tool_repository_credentials",
|
||||
"copy_template",
|
||||
"enable_prompt_line_editing",
|
||||
"ensure_project_id",
|
||||
"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",
|
||||
@@ -48,6 +53,32 @@ console = Console()
|
||||
_TEMPLATE_TOKEN_RE = re.compile(r"{{([a-zA-Z_][a-zA-Z0-9_]*)}}")
|
||||
|
||||
|
||||
def ensure_project_id(pyproject_path: str | Path = "pyproject.toml") -> str | None:
|
||||
"""Return the project's id, minting one and telling the user if it was added.
|
||||
|
||||
Safe to call from any CLI command: returns None rather than raising when
|
||||
there is no project, or when ``pyproject.toml`` is not writable.
|
||||
|
||||
Args:
|
||||
pyproject_path: Path to the project's ``pyproject.toml``.
|
||||
|
||||
Returns:
|
||||
The project id, or None if one could not be read or created.
|
||||
"""
|
||||
project_id, created = get_or_create_project_id(pyproject_path)
|
||||
|
||||
if created:
|
||||
console.print(
|
||||
f"Added [bold]project_id[/bold] to {pyproject_path} under "
|
||||
# Escaped: Rich would otherwise parse [tool.crewai] as a style tag.
|
||||
r"[bold]\[tool.crewai][/bold] so this project's runs and traces stay "
|
||||
"linked. Commit it to share that link with your team.",
|
||||
style="dim",
|
||||
)
|
||||
|
||||
return project_id
|
||||
|
||||
|
||||
def is_dmn_mode_enabled() -> bool:
|
||||
"""Return True when the enterprise non-interactive mode is enabled."""
|
||||
value = os.environ.get("CREWAI_DMN")
|
||||
|
||||
Reference in New Issue
Block a user