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
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
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