Commit Graph

5 Commits

Author SHA1 Message Date
7896b9f7f1 feat(bundles): metapackage split (Phase A) — engine-only lfx, lfx-bundles long tail, 5 graduated partner packages (#13563)
* feat(extension): add manifest-less lfx.bundles discovery + precedence tier

Foundation for the bundle metapackage split (1.11). Adds a third
@official-slot discovery source: a distribution declaring the
[project.entry-points."lfx.bundles"] group ships a package whose immediate
subdirectories are each a manifest-less bundle, folder-walked and registered
at @official with no extension.json (the langchain-community model).

- new loader/_bundles_root.py::load_lfx_bundles_extensions, mirroring
  discover_inline_bundles but sourcing roots from the lfx.bundles entry-point
  group via find_spec and reusing _load_bundle_directory
- discovery precedence becomes installed > seed > lfx_bundles > dev > inline
  so a manifest-shipping lfx-<provider> always shadows the same-named
  provider in the metapackage (lets a provider graduate with no lockstep
  release), emitting the existing bundle-shadowed warning
- new bundle-discovery-malformed warning code, emitted on warnings (never
  flips ok / aborts startup) for unresolvable declarations and invalid
  provider folder names
- exported via loader/__init__ and the lfx.extension PEP-562 lazy surface

10 new tests in test_load_lfx_bundles.py; full extension unit suite (449)
passes; ruff + format clean; mypy clean on the new module.

* docs(bundles): changelog entry for the lfx.bundles discovery surface

The BUNDLE_API.md changelog gate diffs each PR's branch against main, so
the entry covering this PR's surface additions (lfx.bundles discovery,
the lfx_bundles precedence tier, bundle-discovery-malformed) must live
on this branch, not only further up the stack. Text is verbatim from the
graduate-partners commit so the stacked merge dedupes trivially.

* fix(extension): reject plain-module lfx.bundles targets; correct validate wording

CodeRabbit review fixes on #13563:
- _spec_package_dir no longer falls back to a plain module's parent
  directory -- a single-file entry-point target now emits the
  bundle-discovery-malformed warning instead of folder-walking unrelated
  sibling directories as bundles (+ regression test)
- BUNDLE_API.md / loader docstring no longer say manifest-less providers
  are 'exempt from lfx extension validate'; they are not valid validator
  input (validate requires a manifest and reports manifest-not-found)

* fix(extension): review fixes — shadowed metapackage providers never import; typed manifest-less reload refusal

Review findings on #13563:

- A manifest-less provider whose bundle name is already claimed by a
  higher-precedence @official source (installed/seed) is now skipped
  WITHOUT importing: all @official sources share the
  _lfx_ext.official.<bundle>.* sys.modules namespace, so importing the
  losing copy overwrote the winner's live modules even though
  _resolve_bundle_shadowing dropped its components afterwards.  The
  orchestrator passes the claimed-name map (built by the new
  _claimed_official_bundles helper, mirroring the resolver's winner
  rule); the skipped copy still emits the typed bundle-shadowed warning.
  This collision is the expected post-graduation state, not an error.

- Manifest-less records now carry a manifestless provenance flag
  (additive field on LoadResult + BundleRecord).  The reload pipeline
  refuses them with the new typed reload-manifestless-unsupported code
  instead of routing through load_extension and failing with a
  misleading manifest-not-found.  Hot reload of metapackage providers
  is a process-restart concern (pip-installed content); a loader-based
  reload path can follow if QA wants it.

BUNDLE_API.md changelog entry extended for both surface changes.

* fix(extension): harden lfx.bundles discovery per review — broad find_spec guard, namespace portions, per-mode error codes

Review findings from ogabrielluiz on #13563:

- find_spec on a dotted declaration imports the PARENT package, whose
  __init__ can raise anything; a non-Import error escaped the old catch
  tuple and (through the palette cache's catch-all) wiped every source's
  components for the boot.  Catch Exception and degrade to the malformed
  sentinel; comment corrected.

- Namespace packages: walk ALL submodule_search_locations portions (one
  root per portion) instead of only locations[0], and dedupe resolved
  roots by path so duplicate entry-point declarations (or overlapping
  portions) never walk a directory twice and self-shadow.
  _spec_package_dir -> _spec_package_dirs.

- Split bundle-discovery-malformed into one code per failure mode,
  mirroring the inline tier: bundles-provider-name-invalid (rename the
  directory), bundles-root-unreadable (check permissions), keeping
  bundle-discovery-malformed for unresolvable declarations (fix the
  entry-point).  Same-tier duplicates get duplicate-lfx-bundles-provider
  instead of overloading bundle-shadowed, whose rendered template
  (format_extension_error renders templates, not ad-hoc messages) was
  wrong on both counts for that case.

- The claimed-bundles cross-source skip now emits bundle-shadowed on
  errors (matching _resolve_bundle_shadowing) so filtering by code never
  mixes severities; bundle-shadowed is already in the CLI warn-only set.

3 new regression tests (raising parent package, namespace portions,
duplicate entry points); BUNDLE_API changelog updated.

* feat(bundles): create lfx-bundles metapackage skeleton (#13564)

* feat(bundles): add lfx-bundles metapackage skeleton

Creates the manifest-less lfx-bundles distribution (the langchain-community
model) that the long-tail providers will move into. Empty skeleton for now;
the bulk move (scripts/migrate/consolidate_bundles.py) populates the provider
folders + per-provider extras later.

- src/bundles/lfx-bundles: a single pyproject declaring the lfx.bundles
  entry-point (lfx_bundles = "lfx_bundles"), an lfx>=1.10.0,<2.0.0 pin, and a
  generated (currently empty) `all` extra; plus a bare lfx_bundles namespace
  package and a README documenting the model + install stories
- wired into the root workspace via the existing bundle marker blocks:
  dep lfx-bundles[all]>=1.0,<2.0, uv source, and member
- hyphen dir name so release.yml's src/bundles/*/pyproject.toml glob builds
  it with zero workflow change

Verified: the wheel builds (entry_points.txt carries the lfx.bundles group);
load_lfx_bundles_extensions (PR-1) discovers the entry point and the empty
skeleton registers zero providers with no error.

* fix(ci): nightly bundle rename follows [extras] refs and self-refs

Two gaps in update_bundle_versions.py around extras suffixes, both fatal
or silently wrong for the first nightly carrying the lfx-bundles
metapackage:

- update_root_pyproject_for_bundle's dep regex required the version
  specifier immediately after the name, so "lfx-bundles[all]>=1.0,<2.0"
  (a MAIN dep) was left unrewritten while the workspace member was
  renamed to lfx-bundles-nightly; uv lock then tries to resolve stable
  lfx-bundles from PyPI, where it does not exist. The same gap silently
  left "lfx-docling[local]>=0.1.0" optional-dep refs pointing at the
  stable distribution. The regex now tolerates an [extras] group and
  carries it into the replacement.

- rename_bundle_pyproject skipped self-referencing extras, so the
  metapackage's generated `all` extra kept 45 "lfx-bundles[<provider>]"
  members after the rename, pulling the stable distribution (same
  lfx_bundles import package, install collision) once published. Self
  refs now follow the rename, idempotently.

Tests drive the real script module, mirroring test_bundle_lfx_pin.py.
This closes the PR-2 audit item flagged when the metapackage was
introduced. The canonical-pre-release cutover would retire the nightly
rename entirely; until it lands, the rename must be correct.

* fix(bundles): address review — drop orphaned nightly bundle-rename script, fix README install stories

- Delete scripts/ci/update_bundle_versions.py and its tests: the nightly
  bundle-rename track it served was retired by the canonical pre-release
  cutover (src/bundles/NIGHTLY.md) and no workflow or Makefile target on
  main or any release branch invokes it. Stale doc references in
  sync_bundle_lfx_pin.py and test_bundle_lfx_pin.py updated to match.
- README: split the install section into what works today (langflow, bare
  lfx) vs what arrives with the bulk move / engine-only split
  (lfx[bundles], per-provider lfx-bundles[<provider>] extras), and note
  the generated all extra is empty until the first provider tranche lands.

* ci(bundles): cross-bundle test matrix (lfx contract axis) (#13566)

* ci(bundles): add cross-bundle test matrix (lfx contract axis)

Tests every extracted bundle (the lfx-bundles metapackage + each graduated
lfx-<provider>) against the lfx contract surface it depends on -- overdue
since 1.10 left 4 independently-versioned bundles depending on lfx.

- .github/workflows/cross-bundle-test.yml: discovers bundles via the same
  src/bundles/*/pyproject.toml glob release.yml uses, then per
  (bundle x python 3.10/3.13): installs the in-repo lfx + the bundle, imports
  the declared entry-point package, asserts lfx.bundles discovery is
  error-free (for the metapackage), runs `lfx extension validate` for
  manifest bundles, and runs the bundle's own tests/.
- Triggers: pull_request (src/bundles, src/lfx), workflow_dispatch, a weekly
  schedule, and workflow_call.

The lfx-minor axis seeds with the in-repo lfx (the 1.10 line is unpublished);
when minors publish, add the version dimension (oldest+latest get full tests,
every supported minor gets contract-smoke) per the epic's cost-control shape.

Verified: the workflow YAML parses and the contract-smoke logic imports a
real bundle (lfx_arxiv) cleanly.

* fix(ci): cross-bundle smoke tolerates extras-less SDK degradation

Review findings on the cross-bundle matrix:

- CRITICAL: the contract smoke asserted zero discovery errors, but the CI
  venv installs lfx-bundles WITHOUT per-provider extras, so providers whose
  modules import their SDK at top level degrade with module-import-failed --
  the expected graceful-degradation contract. The smoke now fails only on
  structural codes and reports the degraded-module count. (Graduated partner
  bundles carry their deps directly, not as extras, so their steps are
  unaffected.)
- the scheduled run now actually delivers the exhaustive grid the header
  promised (all supported Pythons on schedule; oldest+latest on PRs)
- concurrency group with cancel-in-progress on PRs so stacked bundle PRs
  don't queue N-bundles x N-pythons jobs per push

* ci(bundles): tomli fallback for the py3.10 cross-bundle smoke

tomllib is stdlib only from Python 3.11, so the contract-smoke heredoc
failed with ModuleNotFoundError on every py3.10 matrix leg. Install the
tomli backport into the smoke venv and import it as a fallback.

* fix(test): coherent sys.modules restore in the ibm without_ibm_db fixture

The cross-bundle matrix's py3.10 leg exposed test-ordering pollution in
the ibm bundle suite: without_ibm_db monkeypatch-deleted three lfx_ibm
modules and re-imported them mid-test, but entries created during the
test survive teardown (delitem with raising=False records nothing for
keys it didn't find) and re-imported parents never regain the submodule
attribute bindings that mock.patch target resolution walks on Python
3.10 (3.11+ mock resolves via sys.modules and tolerates the
incoherence). Two fixture uses in sequence left the tree inconsistent
and 29 later watsonx tests failed with AttributeError on the package.

Snapshot, drop, and restore the entire lfx_ibm module tree wholesale
instead. Full suite now passes on both 3.10 and 3.13.

* feat(lfx): add lfx[bundles] extra and keep lfx engine-only (#13565)

* feat(bundles): add lfx-bundles metapackage skeleton

Creates the manifest-less lfx-bundles distribution (the langchain-community
model) that the long-tail providers will move into. Empty skeleton for now;
the bulk move (scripts/migrate/consolidate_bundles.py) populates the provider
folders + per-provider extras later.

- src/bundles/lfx-bundles: a single pyproject declaring the lfx.bundles
  entry-point (lfx_bundles = "lfx_bundles"), an lfx>=1.10.0,<2.0.0 pin, and a
  generated (currently empty) `all` extra; plus a bare lfx_bundles namespace
  package and a README documenting the model + install stories
- wired into the root workspace via the existing bundle marker blocks:
  dep lfx-bundles[all]>=1.0,<2.0, uv source, and member
- hyphen dir name so release.yml's src/bundles/*/pyproject.toml glob builds
  it with zero workflow change

Verified: the wheel builds (entry_points.txt carries the lfx.bundles group);
load_lfx_bundles_extensions (PR-1) discovers the entry point and the empty
skeleton registers zero providers with no error.

* feat(lfx): add lfx[bundles] extra and keep lfx engine-only

lfx ships no bundles by default. The new optional extra pulls the long-tail
metapackage for users who want the provider components without the full
langflow server install.

- src/lfx/pyproject.toml: [project.optional-dependencies]
  bundles = ["lfx-bundles[all]>=1.0,<2.0"]; intentionally NO lfx[all]
- pip install lfx          -> engine only
  pip install "lfx[bundles]" -> engine + the lfx-bundles long tail
  (documented as a headless/serverless deployment footnote, not a headline)

Verified against the built lfx wheel METADATA: lfx-bundles appears only under
`extra == "bundles"`, never in core Requires-Dist, so the engine-only default
is preserved.

* fix(ci): nightly bundle rename follows [extras] refs and self-refs

Two gaps in update_bundle_versions.py around extras suffixes, both fatal
or silently wrong for the first nightly carrying the lfx-bundles
metapackage:

- update_root_pyproject_for_bundle's dep regex required the version
  specifier immediately after the name, so "lfx-bundles[all]>=1.0,<2.0"
  (a MAIN dep) was left unrewritten while the workspace member was
  renamed to lfx-bundles-nightly; uv lock then tries to resolve stable
  lfx-bundles from PyPI, where it does not exist. The same gap silently
  left "lfx-docling[local]>=0.1.0" optional-dep refs pointing at the
  stable distribution. The regex now tolerates an [extras] group and
  carries it into the replacement.

- rename_bundle_pyproject skipped self-referencing extras, so the
  metapackage's generated `all` extra kept 45 "lfx-bundles[<provider>]"
  members after the rename, pulling the stable distribution (same
  lfx_bundles import package, install collision) once published. Self
  refs now follow the rename, idempotently.

Tests drive the real script module, mirroring test_bundle_lfx_pin.py.
This closes the PR-2 audit item flagged when the metapackage was
introduced. The canonical-pre-release cutover would retire the nightly
rename entirely; until it lands, the rename must be correct.

* ci(bundles): freeze lfx/components against new top-level providers (#13567)

* feat(bundles): add lfx-bundles metapackage skeleton

Creates the manifest-less lfx-bundles distribution (the langchain-community
model) that the long-tail providers will move into. Empty skeleton for now;
the bulk move (scripts/migrate/consolidate_bundles.py) populates the provider
folders + per-provider extras later.

- src/bundles/lfx-bundles: a single pyproject declaring the lfx.bundles
  entry-point (lfx_bundles = "lfx_bundles"), an lfx>=1.10.0,<2.0.0 pin, and a
  generated (currently empty) `all` extra; plus a bare lfx_bundles namespace
  package and a README documenting the model + install stories
- wired into the root workspace via the existing bundle marker blocks:
  dep lfx-bundles[all]>=1.0,<2.0, uv source, and member
- hyphen dir name so release.yml's src/bundles/*/pyproject.toml glob builds
  it with zero workflow change

Verified: the wheel builds (entry_points.txt carries the lfx.bundles group);
load_lfx_bundles_extensions (PR-1) discovers the entry point and the empty
skeleton registers zero providers with no error.

* ci(bundles): freeze lfx/components against new top-level providers

After the metapackage split, new providers go to lfx-bundles (or a graduated
lfx-<provider>), never in-tree. Adds an additions-only CI gate.

- scripts/ci/check_components_frozen.py: stdlib gate comparing the live
  top-level dir listing of src/lfx/src/lfx/components/ against a committed
  baseline; fails on any directory not in the baseline. Removals are allowed
  so M4 shim cleanup (which shrinks the set) never trips it.
- scripts/ci/frozen_component_dirs.txt: the 111-dir baseline.
- .github/workflows/lint-py.yml: new freeze-components job runs the gate
  (stdlib-only, no uv sync).

Verified locally: passes on the baseline, fails (exit 1) on a simulated new
provider directory with an actionable message, passes again after cleanup.

* fix(ci): freeze gate counts only dirs shipping __init__.py

Review finding: a stray directory holding only __pycache__ bytecode (left
behind by a branch switch) tripped the additions-only gate locally. A
directory without __init__.py is not a provider; ignore it.

* fix(ci): nightly bundle rename follows [extras] refs and self-refs

Two gaps in update_bundle_versions.py around extras suffixes, both fatal
or silently wrong for the first nightly carrying the lfx-bundles
metapackage:

- update_root_pyproject_for_bundle's dep regex required the version
  specifier immediately after the name, so "lfx-bundles[all]>=1.0,<2.0"
  (a MAIN dep) was left unrewritten while the workspace member was
  renamed to lfx-bundles-nightly; uv lock then tries to resolve stable
  lfx-bundles from PyPI, where it does not exist. The same gap silently
  left "lfx-docling[local]>=0.1.0" optional-dep refs pointing at the
  stable distribution. The regex now tolerates an [extras] group and
  carries it into the replacement.

- rename_bundle_pyproject skipped self-referencing extras, so the
  metapackage's generated `all` extra kept 45 "lfx-bundles[<provider>]"
  members after the rename, pulling the stable distribution (same
  lfx_bundles import package, install collision) once published. Self
  refs now follow the rename, idempotently.

Tests drive the real script module, mirroring test_bundle_lfx_pin.py.
This closes the PR-2 audit item flagged when the metapackage was
introduced. The canonical-pre-release cutover would retire the nightly
rename entirely; until it lands, the rename must be correct.

* feat(bundles): move 45 long-tail providers into lfx-bundles + graduate 5 partner packages (#13568)

* feat(bundles): add lfx-bundles metapackage skeleton

Creates the manifest-less lfx-bundles distribution (the langchain-community
model) that the long-tail providers will move into. Empty skeleton for now;
the bulk move (scripts/migrate/consolidate_bundles.py) populates the provider
folders + per-provider extras later.

- src/bundles/lfx-bundles: a single pyproject declaring the lfx.bundles
  entry-point (lfx_bundles = "lfx_bundles"), an lfx>=1.10.0,<2.0.0 pin, and a
  generated (currently empty) `all` extra; plus a bare lfx_bundles namespace
  package and a README documenting the model + install stories
- wired into the root workspace via the existing bundle marker blocks:
  dep lfx-bundles[all]>=1.0,<2.0, uv source, and member
- hyphen dir name so release.yml's src/bundles/*/pyproject.toml glob builds
  it with zero workflow change

Verified: the wheel builds (entry_points.txt carries the lfx.bundles group);
load_lfx_bundles_extensions (PR-1) discovers the entry point and the empty
skeleton registers zero providers with no error.

* feat(bundles): consolidate first long-tail tranche into lfx-bundles

Adds scripts/migrate/consolidate_bundles.py (the inverse of port_bundle.py --
moves in-tree providers into the manifest-less lfx-bundles metapackage) and
runs it on a verified 5-provider tranche: tavily, exa, wikipedia, yahoosearch,
wolframalpha.

Per provider the script:
- moves src/lfx/src/lfx/components/<p>/ -> lfx_bundles/<p>/ (lowercase names);
- leaves a fail-soft import shim (first line `# lfx-bundles-shim`) so
  `from lfx.components.<p> import X` keeps working when lfx-bundles is
  installed, and raises an actionable ImportError otherwise;
- merges the provider's third-party deps into a PEP 685-normalized lfx-bundles
  extra and regenerates the `all` aggregate. Dep parity holds: `uv sync` is a
  no-op because those deps were already pulled via langflow-base[complete];
- appends the 4-entry migration block per Component class (28 entries) so saved
  flows referencing lfx.components.<p>.<Class> migrate to
  ext:<p>:<Class>@official.

To avoid double registration, the in-tree component walk
(_load_components_dynamically) now skips shimmed provider dirs, and
component_index.json is regenerated (355->348 components, 95->90 modules); the
moved providers load only at @official via lfx.bundles discovery.

Verified: discovery finds all 5 at @official with no errors; shims resolve;
`import lfx.components` still works; the index drops the 7 moved component
entries (residual `tools`-category name refs resolve via the shim); ruff clean.

First tranche proves the engine; the remaining long-tail scales by extending
PROVIDER_DEPS (each provider's deps verified individually -- the careful part).

* feat(bundles): consolidate 30-provider tranche 2 into lfx-bundles

Extends PROVIDER_DEPS with 30 individually-verified providers and runs the
consolidation: vector stores (chroma, clickhouse, couchbase, milvus, mongodb,
pgvector, pinecone, qdrant, supabase, upstash, weaviate), model providers
(groq, mistral, ollama, perplexity, sambanova), and tools/memory/data (apify,
assemblyai, confluence, firecrawl, git, glean, icosacomputing, mem0, needle,
scrapegraph, serpapi, unstructured, youtube, zep).

Dep verification (the careful part): every spec comes from langflow-base's
per-provider extras or its direct dependencies; langchain-community providers
carry the wrapper plus the SDK the wrapper lazy-imports (e.g. pgvector,
atlassian-python-api); requests is declared explicitly where imported (it is
only transitive in today's env); pinecone keeps its python_version<'3.14'
marker verbatim. Tranche excludes: providers with langflow imports (vlmrun),
provider-specific lfx.base dirs (composio/huggingface/langwatch), case-
sensitive names (FAISS/Notion), the openai-SDK family (azure/aiml/deepseek/
litellm/lmstudio/novita/openrouter/vllm/xai/cometapi -- cleaner after PR-8),
and the partner set.

Dep parity verified at the resolution level: the uv.lock diff is +220 lines of
lfx-bundles extras metadata with ZERO packages added or removed (`name =` diff
empty), so pip install langflow resolves the identical set.

Also: 192 append-only migration entries (48 classes x 4, zero bare-name
ambiguities); component_index.json regenerated 348->300 components / 90->60
modules (exactly the moved set, no stale standalone entries); mongodb_atlas.py
SLF001 per-file-ignore and the mem0/mongodb detect-secrets baseline entries
migrated to the new paths (lint/secrets exceptions travel with moved files);
35 bundles now discover at @official with 55 components; shims verified across
categories; 449 extension tests pass.

* feat(bundles): consolidate openai-SDK family tranche 3 into lfx-bundles

10 providers that ride the langchain-openai wrapper, deferred from tranche 2
until the partner graduation settled the openai-SDK dep story: aiml, azure,
cometapi, deepseek, litellm, lmstudio, novita, openrouter, vllm, xai.

Dep verification: every provider declares langchain-openai>=1.1.6; the openai
SDK is declared only where a component imports it directly (aiml, deepseek,
litellm, lmstudio, vllm, xai -- wrapper-transitive elsewhere); requests
declared where imported (cometapi, deepseek, novita, xai); lmstudio's lazy
NVIDIAEmbeddings path gets langchain-nvidia-ai-endpoints~=1.0.0. The litellm
component drives LiteLLM-served endpoints through the OpenAI client and does
NOT import the litellm package -- langflow-base's litellm extra stays put.
These providers use the lazy _dynamic_imports __init__ shape; it survives the
move unchanged (import_mod resolves via __spec__.parent and
lfx.components._importing remains a core helper).

Dep parity: uv.lock diff has zero package additions/removals (all specs
already resolved via langflow-base[complete]).

Also: 56 append-only migration entries (14 classes x 4, zero ambiguities);
component_index.json regenerated 300->286 components / 60->50 modules (exactly
the moved set); detect-secrets baseline re-keyed; 45 bundles now discover at
@official with 69 components; shims verified (azure/xai/litellm/deepseek);
ruff clean.

* fix(ci): nightly bundle rename follows [extras] refs and self-refs

Two gaps in update_bundle_versions.py around extras suffixes, both fatal
or silently wrong for the first nightly carrying the lfx-bundles
metapackage:

- update_root_pyproject_for_bundle's dep regex required the version
  specifier immediately after the name, so "lfx-bundles[all]>=1.0,<2.0"
  (a MAIN dep) was left unrewritten while the workspace member was
  renamed to lfx-bundles-nightly; uv lock then tries to resolve stable
  lfx-bundles from PyPI, where it does not exist. The same gap silently
  left "lfx-docling[local]>=0.1.0" optional-dep refs pointing at the
  stable distribution. The regex now tolerates an [extras] group and
  carries it into the replacement.

- rename_bundle_pyproject skipped self-referencing extras, so the
  metapackage's generated `all` extra kept 45 "lfx-bundles[<provider>]"
  members after the rename, pulling the stable distribution (same
  lfx_bundles import package, install collision) once published. Self
  refs now follow the rename, idempotently.

Tests drive the real script module, mirroring test_bundle_lfx_pin.py.
This closes the PR-2 audit item flagged when the metapackage was
introduced. The canonical-pre-release cutover would retire the nightly
rename entirely; until it lands, the rename must be correct.

* feat(bundles): graduate partner set to standalone lfx-<provider> packages (#13573)

* feat(bundles): graduate partner set to standalone lfx-<provider> packages

Extracts the five partner/flagship providers into manifest-shipping
distributions: lfx-openai, lfx-anthropic, lfx-amazon, lfx-datastax,
lfx-cohere. Each ships extension.json (lfx.compat ["1"]), a
langflow.extensions entry-point, an lfx>=1.10.0,<2.0.0 pin, and is wired into
the root workspace marker blocks. Zero flow impact by the bundle-name
invariant: ext:<provider>:<Class>@official ids are unchanged.

Mechanics:
- adopts the five-phase scripts/migrate/port_bundle.py from
  feat/bundle-mass-extraction (handles shared-base moves, consumer rewrites,
  surgical index updates, migration-table appends) and runs it per partner
  with --migration-release 1.11.0
- datastax's shared lfx.base.datastax moves into lfx_datastax.base; its
  backend unit tests move to src/bundles/datastax/tests (74 pass); its ruff
  per-file-ignore and its check_component_env_writes ALLOWLIST entry travel;
  10 repo consumers rewritten (incl. the vector_store_rag starter project)
- amazon_bedrock_converse.py legacy langflow.* imports rewritten to the lfx.*
  equivalents (thin re-export aliases; behavior-identical) pre-extraction
- runtime deps pinned from langflow-base's extras / direct deps (wrapper-
  guaranteed SDKs stay transitive, parity-exact); --remove-base-extra dropped
  the openai/anthropic/cohere/aws extras from langflow-base[complete] in favor
  of the bundle pins; the astradb extra stays (also used outside datastax)
- in-tree dirs replaced with marker shims pointing at lfx_<p>.components.<p>
  (skipped by the in-tree walk; legacy from lfx.components.<p> imports keep
  working); lfx/components/__init__.py entries kept, consistent with the
  consolidated providers
- validate.py: accept classes whose base is a *derived* Component base
  (LCVectorStoreComponent / LCToolComponent / ...) -- they inherit the
  class-level outputs declaration and only override the output method, which
  the AST-only check could not see; bare Component subclasses keep the strict
  build/outputs requirement (+ regression test)
- BUNDLE_API.md changelog entries added for the branch's surface changes: the
  lfx.bundles manifest-less discovery group + precedence tier +
  bundle-discovery-malformed code (from the foundation PR) and the validator
  acceptance above
- 100 append-only migration entries (20 classes x 5 partners x 4 shapes);
  component_index.json surgically updated 300->280 components / 60->55
  modules, sha256 recomputed and verified
- detect-secrets baseline re-keyed for moved partner files

Dep parity: lock diff adds only the five lfx-* package names; no third-party
package added or removed. One benign transitive re-resolution: anthropic SDK
0.105.2 -> 0.109.0 (allowed by langchain-anthropic's range on both sides).

Verified: all five register at @official via the installed-manifest tier
(extension_id/distribution = lfx-<p>, 20 components); manifest-less
lfx_bundles unchanged (35 bundles, disjoint); all 5 shims import; engine-safe;
`lfx extension validate` passes for all five; 450 extension unit tests, 60
pilot-upgrade migration tests, 74 moved datastax tests pass; ruff clean.

* fix(bundles): shim lfx.base.datastax so stored-flow imports keep resolving

The datastax graduation moved lfx.base.datastax into lfx_datastax.base,
but saved flows and starter templates (Hybrid Search RAG,
TemplateAssistant) embed `from lfx.base.datastax.astradb_base import
AstraDBBaseComponent` inside their stored component code fields, which
is re-executed verbatim at flow build time. Without a shim that import
raises ModuleNotFoundError and the flows fail to build
(test-starter-projects red).

Mirror the lfx.components shim contract: module-aliasing to
lfx_datastax.base, narrow except that only translates a missing bundle
(transitive dep failures re-raise untouched), marker-tagged single-file
dir, removed at M4 together with the components shims.

* chore(bundles): bounded version ranges for curated lfx-* packages (#13576)

langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.

- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
  follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
  contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
  CI matrix verifies the ranges resolve together across the supported lfx axis

Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.

* test(bundles): lock the in-tree shim contract + breakage audit (#13577)

* chore(bundles): bounded version ranges for curated lfx-* packages

langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.

- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
  follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
  contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
  CI matrix verifies the ranges resolve together across the supported lfx axis

Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.

* test(bundles): lock the in-tree shim contract + breakage audit

Locks the five-point contract for the 50 lfx-bundles import shims under
lfx/components/ (45 metapackage + 5 graduated partners):

  1. first line is the `# lfx-bundles-shim` marker (keys the in-tree walk's
     double-registration skip);
  2. a shim dir is exactly one __init__.py -- no impls, no deps;
  3. sys.modules module-aliasing (submodule trees resolve);
  4. bundle missing -> actionable ModuleNotFoundError whose wording is part
     of the contract ("pip install lfx-bundles" / "pip install lfx-<p>");
  5. a missing *transitive* dep re-raises untouched.

Test shape (env-adaptive by design -- the lfx test env prunes the venv, so
nothing here assumes bundles are installed):
- source-level sweep of every real shim, parameterized (no imports);
- hermetic mechanism tests against a synthetic shim + synthetic target
  (alias-resolves / locked-message / transitive-reraise);
- the walk-skip detector and the marker sweep must agree exactly;
- one adaptive live test on lfx.components.tavily exercising whichever
  branch the running env is in.

106 tests pass in the pruned lfx env (where the live test exercises the
bare-engine locked-message branch).

Breakage audit (gh code search, recorded in the PR): ~20-25 public repos
reference lfx.components.*; the majority are langflow forks (vendored tree,
unaffected by packaging). Genuine library consumers exist and the moved
providers have real surface (openai: 11 external repos, datastax: 17) --
covered by the shims wherever bundles are co-installed (every
`pip install langflow`). Decision: deprecation warnings NOT warranted now;
revisit at M4 (shim removal), where one loud minor release before removal
is the right shape.

* test(bundles): extend the shim contract sweep to lfx.base shims

The datastax graduation moved lfx.base.datastax into lfx_datastax.base
and left a module-aliasing shim behind (stored flow code fields embed
the legacy import and are re-executed verbatim at build time). Sweep
lfx/base for marker shims with the same source-level contract as the
components sweep: one-file stub, module-aliasing to the bundle's base
subpackage, narrow name-checked except, locked install message.

* docs(bundles): install shapes, override rule, bundle_api_version (#13578)

Documents the deliberately small user-facing rule surface of the metapackage
split (1.11):

- extensions-overview.mdx: the two install stories (pip install langflow =
  everything, same as today; pip install lfx = engine only, bring your own
  bundles); the bundle-name-is-identity invariant; the current package table
  (lfx-bundles metapackage + the 5 partner packages + the 4 pilots); the
  legacy-import shim behavior with the locked ModuleNotFoundError wording and
  the migration recipe for direct lfx users (switch to lfx[bundles] or pin
  lfx-<provider> packages); the override rule ("ship a manifest -- a manifest
  always wins", with the bundle-shadowed warning); bundle_api_version as the
  single compat number (lfx.compat ["1"]; manifest-less packages ride their
  PEP 508 lfx pin) and the no-version-arithmetic rule.
- deployment-lfx-compatibility.mdx: lfx[bundles] as the headless/serverless
  deployment footnote (engine + lfx-bundles[all]; slimmer per-provider
  alternative; intentionally no lfx[all]).

Both files verified MDX-safe (no unbackticked angle brackets).

* fix(bundles): stack-review fixes — symlink containment, idempotency, docs accuracy (#13579)

* chore(bundles): bounded version ranges for curated lfx-* packages

langflow's pyproject is the release-coordination point now that lfx and the
lfx-* bundles release independently. Every curated lfx-* dependency declares a
BOUNDED range (>=A,<B), never an exact pin -- exact pins in reusable library
metadata create resolver conflicts for downstreams that depend on a different
version. Exact pins for reproducibility live in uv.lock and the release-build
manifests.

- the 4 pilots + 5 graduated partners: >=0.1.0 -> >=0.1.0,<1.0.0 (bundles
  follow their own 0.1.x cadence)
- lfx-bundles[all]>=1.0,<2.0 unchanged (versions with the metapackage
  contract)
- the three lfx-docling[...] optional-dependency refs bounded the same way
- policy documented inline in the bundle-deps marker block; the cross-bundle
  CI matrix verifies the ranges resolve together across the supported lfx axis

Verified: uv lock resolves with zero package/version changes (bounds are
metadata-only today); the nightly rename's dep regex already matches the
bounded form.

* test(bundles): lock the in-tree shim contract + breakage audit

Locks the five-point contract for the 50 lfx-bundles import shims under
lfx/components/ (45 metapackage + 5 graduated partners):

  1. first line is the `# lfx-bundles-shim` marker (keys the in-tree walk's
     double-registration skip);
  2. a shim dir is exactly one __init__.py -- no impls, no deps;
  3. sys.modules module-aliasing (submodule trees resolve);
  4. bundle missing -> actionable ModuleNotFoundError whose wording is part
     of the contract ("pip install lfx-bundles" / "pip install lfx-<p>");
  5. a missing *transitive* dep re-raises untouched.

Test shape (env-adaptive by design -- the lfx test env prunes the venv, so
nothing here assumes bundles are installed):
- source-level sweep of every real shim, parameterized (no imports);
- hermetic mechanism tests against a synthetic shim + synthetic target
  (alias-resolves / locked-message / transitive-reraise);
- the walk-skip detector and the marker sweep must agree exactly;
- one adaptive live test on lfx.components.tavily exercising whichever
  branch the running env is in.

106 tests pass in the pruned lfx env (where the live test exercises the
bare-engine locked-message branch).

Breakage audit (gh code search, recorded in the PR): ~20-25 public repos
reference lfx.components.*; the majority are langflow forks (vendored tree,
unaffected by packaging). Genuine library consumers exist and the moved
providers have real surface (openai: 11 external repos, datastax: 17) --
covered by the shims wherever bundles are co-installed (every
`pip install langflow`). Decision: deprecation warnings NOT warranted now;
revisit at M4 (shim removal), where one loud minor release before removal
is the right shape.

* docs(bundles): install shapes, override rule, bundle_api_version

Documents the deliberately small user-facing rule surface of the metapackage
split (1.11):

- extensions-overview.mdx: the two install stories (pip install langflow =
  everything, same as today; pip install lfx = engine only, bring your own
  bundles); the bundle-name-is-identity invariant; the current package table
  (lfx-bundles metapackage + the 5 partner packages + the 4 pilots); the
  legacy-import shim behavior with the locked ModuleNotFoundError wording and
  the migration recipe for direct lfx users (switch to lfx[bundles] or pin
  lfx-<provider> packages); the override rule ("ship a manifest -- a manifest
  always wins", with the bundle-shadowed warning); bundle_api_version as the
  single compat number (lfx.compat ["1"]; manifest-less packages ride their
  PEP 508 lfx pin) and the no-version-arithmetic rule.
- deployment-lfx-compatibility.mdx: lfx[bundles] as the headless/serverless
  deployment footnote (engine + lfx-bundles[all]; slimmer per-provider
  alternative; intentionally no lfx[all]).

Both files verified MDX-safe (no unbackticked angle brackets).

* fix(bundles): review fixes — symlink containment, idempotency, docs accuracy

Fixes from the multi-agent stack review (kept as a separate PR so the
reviewed PRs' diffs stay frozen for QA's independent pass):

- _bundles_root.py: directory-level symlink containment — a provider dir
  that resolves outside the bundle root is skipped with a typed
  bundle-discovery-malformed warning, mirroring the seed-directory walk's
  is_within rule (+ regression test). Anchors the trust boundary to the
  installed package tree.
- consolidate_bundles.py: migration-append idempotency guard — entries are
  deduped on full content so a partially-failed earlier run cannot duplicate
  rows on re-run (table stays append-only).
- langflow-base pyproject: documented WHY the aws extra is deliberately
  retained after the lfx-amazon graduation (boto3 also backs the server's
  own S3 storage backend and lfx's S3 ingestion — review suggested removing
  it; that would regress S3 storage support).
- extensions-overview.mdx: note that `lfx extension list` shows
  manifest-shipping extensions only; manifest-less packages load at startup
  but are not listed.

93 loader+migration tests pass (incl. the new symlink test); ruff clean.

* refactor(bundles): stabilize import_mod as a public BUNDLE_API utility

The lazy-import helper that bundle packages call from their
__getattr__-based __init__.py files lived at lfx.components._importing -- an
internal path with no stability contract, imported by 35 separately-installed
bundle __init__ files (30 lfx-bundles providers + the 5 graduated partners).

- canonical home is now lfx.utils.lazy_import.import_mod (code unchanged);
  lfx.components._importing re-exports it so in-tree callers and any external
  code on the old path keep working
- all 35 bundle-side imports rewritten to the stable path
- BUNDLE_API.md: surface table entry + changelog (additive)
- contract tests: re-export identity, both call forms, the AttributeError
  conversion

Verified: identity holds across both paths; lazy __init__ loads work through
the new path for both bundle families; 110 tests pass; ruff clean.

* fix(bundles): tombstone the broken legacy ZepChatMemory build method (#13580)

build_message_history targeted the zep-python v1 SDK (ZepClient +
zep_python.langchain.ZepChatMessageHistory); both were removed in
zep-python 2.x and the zep extra pins 2.0.2, so the method has been
unable to run for as long as the pin has existed -- its ImportError
guard misleadingly told users to 'pip install zep-python' (already
installed). The component is legacy=True with helpers.Memory as its
designated replacement, so rather than hand-write a new integration
against the 2.x SDK, the method now raises a clear RuntimeError
pointing at the Message History component.

Flow identity is preserved: class/component name, display_name,
description, inputs and the memory output are byte-identical, so saved
flows keep loading, i18n locale keys are unchanged, and
migration_table.json needs no edits. New bundle tests pin the stub
contract (identity, actionable error, no zep_python import).

* test(bundles): extend the shim contract sweep to lfx.base shims

The datastax graduation moved lfx.base.datastax into lfx_datastax.base
and left a module-aliasing shim behind (stored flow code fields embed
the legacy import and are re-executed verbatim at build time). Sweep
lfx/base for marker shims with the same source-level contract as the
components sweep: one-file stub, module-aliasing to the bundle's base
subpackage, narrow name-checked except, locked install message.

* test(bundles): extras-drift guard for the lfx-bundles metapackage

Risk-2 of the metapackage split: the generated `all` extra and the
per-provider extras must never drift by hand-edit, or `pip install
langflow` silently loses a provider's deps. Guard the four invariants:
extras <-> provider dirs (PEP 685-normalized), `all` == the exact
self-ref set, normalized keys collision-free, and the metapackage
provider set disjoint from the graduated partner distributions.

* fix(extension): symlink-escaped providers reject with path-escape, matching the documented contract

The lfx.bundles provider containment check (stack-review symlink fix)
emitted bundle-discovery-malformed, whose template and hint describe a
broken entry-point declaration.  The changelog's path-safety entry
already documents symlink escapes as path-escape on every other
discovery path; use the same code here.  Also resolves the semantic
merge conflict with the per-mode code split (the removed
_malformed_error location kwarg).

* fix(tests): repoint lfx tests off moved providers; complete provider fallback map

CI fail-fast had been masking these: the LFX test job runs in an
engine-only env where openai/anthropic/chroma are now bundle-package
shims, so every test that used them as the example category failed on
all Python versions (3.14 just reported first), and the backend Group 2
leg failed collecting test_lfx_bundles_extras.py on Python 3.10.

- flow_requirements: complete _PROVIDER_PACKAGE_FALLBACKS for the nine
  moved model providers (OpenAI, Anthropic, Amazon Bedrock, Groq,
  Google Generative AI, SambaNova, IBM watsonx.ai, Ollama + existing
  Azure OpenAI). In engine-only installs MODEL_PROVIDERS_DICT registers
  only in-tree providers, so the dynamic source-inspection path cannot
  resolve moved providers; the static fallbacks keep lfx run/serve
  requirements inference working. A dict hit still wins.
- test_dynamic_imports / test_import_utils: use composio (still-in-tree
  lazy category with its SDK absent in the bare env) as the example
  category instead of openai/anthropic/chroma; patch import_module at
  its canonical home lfx.utils.lazy_import.
- flow-builder tests (build_flow_from_spec, flow_builder_tools,
  propose_field_edit): specs use LanguageModelComponent (in-tree)
  instead of OpenAIModel.
- test_lfx_bundles_extras: tomli fallback for Python 3.10 (tomllib is
  stdlib 3.11+; pytest guarantees tomli on <3.11).

Full lfx unit suite: 4550 passed. Backend extras+pin tests: 24 passed.

* fix(tests): repoint MCP client-server tests off graduated OpenAIModel key

Same fail-fast-masked class as 14dfa7f268, backend side: since the
partner graduation, extension components register in /api/v1/all under
their namespaced ids (ext:openai:OpenAIModelComponent@official), so the
bare 'OpenAIModel' key these tests hardcoded no longer exists on any
Python version (the 'py3.14-only' Group 2 failure was the only leg that
ran; 3.10 was cancelled by fail-fast). The MCP feature itself is fine —
search returns the namespaced ids and add_component accepts them.

- Use LanguageModelComponent (in-tree, stable bare key; SecretStr
  api_key, real_time_refresh fields, advanced 'stream', LanguageModel
  output) for the redaction/configure/search/describe/spec tests.
- configure_dynamic_field: model_name -> api_key (the refresh field on
  LanguageModelComponent).
- prompt-template-variables spec: drop the model node — server-side
  validation builds the graph and a model without an API key fails its
  build; the test's subject is the dynamic {var} fields.

Full file: 69 passed.

* Update test_mongodb_atlas.py

* Frontend tests

* Frontend test updates

* fix: restore loading.py and tableAutoCellRender to base-branch state

Commit 9df9475615 ('Frontend test updates') accidentally reverted both
files to their pre-#9902 state, removing the no_env_fallback contract in
load_from_env_vars / update_table_params_with_load_from_db_fields, the
defensive params.pop('code') in build_custom_component, and the
localValue/updateGlobalVariableCell handling in tableAutoCellRender.
The base branch's regression tests (test_loading_no_env_fallback.py,
test_loading_custom_component_code_param.py, tableAutoCellRender
index.test.tsx) were kept, so the reversion failed them.

* ci(bundles): make freeze gate reachable on PRs; review follow-ups

- Move the freeze-components job from lint-py.yml (workflow_call/
  workflow_dispatch only -- never invoked, so the gate could not block a
  PR) into extension-migration-checks.yml, which already triggers on
  pull_request for src/lfx/src/lfx/components/**. Add the gate's script
  and baseline to the path filter.
- Add timeout-minutes to the cross-bundle-test jobs so a hung install or
  test leg cannot occupy a runner for the 360-minute default.
- _discover_shimmed_component_dirs: read only the first line of each
  __init__.py instead of the whole file -- the shim marker contract is
  line 1 (enforced by test_shim_source_contract), and this prevents a
  non-shim file with the marker after leading blank lines from being
  misclassified and silently skipped from the in-tree walk.

* fix(bundles): restore legacy-name resolution for ext components; sync starter projects

Saved flows reference moved providers by their legacy palette identity --
either the bare class name (TavilySearchComponent) or the component's
name attribute (AstraDB, Chroma, needle, OllamaModel). After the provider
move neither resolved to a current template on the backend:

- lfx.utils.component_aliases now derives the bare class name (and its
  Component-stripped form) from ext:<bundle>:<Class>@<slot> keys,
  mirroring the frontend's getTemplateAliases. Ext templates carry
  name=None / _type='Component', so the key is the only source.
- _decorate_template_with_extension now stamps template['name'] with the
  component's legacy identity (name attr, falling back to class name).
  In-tree palette entries expose it as the dict key; ext entries are
  keyed by namespaced id, so without this both alias maps lose the only
  bridge from node types like 'AstraDB' to the current template.

Without these, the starter-project updater could not match moved-provider
nodes, so their embedded code stayed one whitespace change behind the
bundle sources -- every such flow showed the update-all banner, and
because tool-mode nodes' saved outputs ([component_as_tool]) differ from
the template's natural outputs the update was flagged breaking, opening
the confirmation modal instead of toasting 'successfully updated':
deterministic timeouts in the starter-project Playwright specs (shards
29/30/36/37). Regenerated the 13 affected starter projects with
scripts/ci/update_starter_projects.py (idempotent; code_hash hex strings
in the regenerated JSONs are detect-secrets false positives).

Also converts test_groq_integration.py to the captured-module
patch.object pattern (same module-identity trap as test_mongodb_atlas:
dotted-name @patch can land on a different materialization of the moved
module than the one that defined the class, leaving the real
get_groq_models in the instance's globals -- order-dependent failure in
unit-test Group 5).

* fix(bundles): keep importable module paths in starter projects

The starter-project updater syncs node metadata from the live templates
(NODE_FORMAT_ATTRIBUTES includes 'metadata'), and ext components report
their runtime sys.modules namespace there (_lfx_ext.official.<p>.<m>) --
a path that only exists inside a running extension loader. Persisting it
broke test_template_field_order_matches_component in both template jobs
('No module named _lfx_ext'): the test imports metadata.module to
instantiate the component, and the migration table likewise keys on the
legacy lfx.components.<provider> form that the bundle shims keep
importable.

_merge_node_metadata now preserves the node's stored module when the
live template's is a runtime _lfx_ext.* path, while still syncing the
rest of the metadata (e.g. dependencies). Starter projects regenerated
from the pre-pollution state with the fixed updater: the only diff vs
the previous regen is the 20 module lines reverting to the legacy
importable form. Regression tests added for both merge directions.

* feat: add Oracle integration (#13502)

* Add Oracle Integration

* [autofix.ci] apply automated fixes

* Fix styleUtils problem

* fix(oracledb): address review feedback

* fix(oracledb): apply formatting

* Fix component index

* refactor(oracledb): convert Oracle integration to lfx-oracle extension bundle

Port the in-tree lfx.components.oracledb provider into a standalone
Extension Bundle distribution (src/bundles/oracle, dist lfx-oracle,
package lfx_oracle, bundle name 'oracle') per src/bundles/PORTING.md:

- Move the four components + connection helper into the bundle; deps
  (oracledb, langchain-oracledb, langchain-community) move from the
  langflow-base[oracledb] extra into the bundle's pyproject
- Add migration-table entries mapping legacy bare class names and
  lfx.components.oracledb.* import paths to ext:oracle:<Class>@official
- Add test_pilot_oracle_upgrade.py integration coverage (mirrors ibm)
- Move backend unit tests into src/bundles/oracle/tests with bundle
  import paths; drop the empty ComponentTestBaseWithoutClient
  version-fixture scaffolding (component is new in 1.11.0)
- Declare explicit outputs on OracleVectorStoreComponent (mirrors
  LCVectorStoreComponent) so lfx extension validate passes
- Regenerate component index (oracledb category removed) and uv.lock;
  wire workspace member/dep/source in root pyproject
- Frontend sidebar entry renamed oracledb -> oracle; docs page renamed
  to bundles-oracle following the extracted-bundle convention

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>

* feat(lfx): fail-fast dependency preflight for `lfx run` (#13680)

Detect a flow's required third-party packages before the graph load and
fail fast with an actionable `pip install ...` message when any are
missing, instead of a deep ModuleNotFoundError mid-build. Addresses the
'export a flow, run it on a bare pip install lfx, it fails' confusion
introduced by the bundle split.

- flow_requirements: add find_missing_dependencies / _is_installed /
  format_missing_dependencies_error (best-effort, reuses the existing
  flow analyzer).
- run.base: _preflight_dependencies helper + check_dependencies gate in
  run_flow (JSON flows only; .py uses PEP 723; fail-open on analysis
  errors so a runnable flow is never blocked by the check itself).
- CLI: --check-dependencies/--no-check-dependencies on `lfx run`.
- tests: unit (find_missing_dependencies/format) + integration (run_flow
  + _preflight_dependencies).

* feat(bundles): move spider + toolguard provider components into lfx-bundles

Two provider components were still living inside otherwise-core directories
(file-level stragglers from the metapackage split). Both are portable
(lfx-only imports) and gated by dedicated optional SDKs, so they belong in
the lfx-bundles metapackage rather than lfx core:

- SpiderTool (langchain_utilities/spider.py) -> lfx_bundles/spider/, dep
  spider-client (`spider` extra).
- PoliciesComponent + policies/ submodule (models_and_agents) ->
  lfx_bundles/toolguard/, dep toolguard (`toolguard` extra).

Changes:
- Move the files into lfx_bundles/<name>/; rewrite policies' absolute
  self-imports (lfx.components.models_and_agents.policies -> lfx_bundles.toolguard.policies).
- Drop SpiderTool / PoliciesComponent from the core langchain_utilities /
  models_and_agents lazy-import registries.
- Add `spider` and `toolguard` extras to lfx-bundles pyproject + the generated
  `all` aggregate; uv.lock re-resolved (uv lock --check clean).
- Append migration entries (4 per class) mapping the historical
  lfx.components.* paths to ext🕷️SpiderTool@official /
  ext:toolguard:PoliciesComponent@official so saved flows migrate.
- Regenerate component_index.json (both classes drop out of core; 263
  components / 45 categories).
- Repoint the toolguard policies tests to lfx_bundles.toolguard (60 pass).

The toolguard deadlock-warmer (_warm_circular_imports) is unaffected: it warms
toolguard.runtime directly and runs single-threaded before the unified
component fan-out that loads bundle components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* feat(bundles): consolidate cleanlab, twelvelabs, jigsawstack into lfx-bundles

First directory-level batch of the core-tail consolidation (validates the
consolidate_bundles.py path end-to-end after the spider/toolguard file-level
extractions). All three are flat, lfx-only, single-SDK providers:

- cleanlab    -> lfx_bundles/cleanlab    (cleanlab-tlm)   3 components
- twelvelabs  -> lfx_bundles/twelvelabs  (twelvelabs)     7 components
- jigsawstack -> lfx_bundles/jigsawstack (jigsawstack)   11 components

Via scripts/migrate/consolidate_bundles.py --apply: moves each dir into the
metapackage + leaves a fail-soft shim, merges per-provider extras and regen
`all`, appends the 4-entry migration block per class (84 entries, 0 ambiguous).
Component index regenerated (45->42 categories, 263->242 core components);
uv.lock re-resolved; twelvelabs test repointed to lfx_bundles.twelvelabs (passes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bundles): consolidate vector/datastore + community-wrapper providers into lfx-bundles

Second directory-level batch (8 providers, 12 components) via
consolidate_bundles.py --apply:

- baidu (qianfan + langchain-community), redis (redis + community),
  elastic (elasticsearch + langchain-elasticsearch + opensearch-py)
- bing, cloudflare, maritalk, searchapi, vectara (langchain-community REST wrappers)

Moves each dir into lfx-bundles + fail-soft shim, merges per-provider extras
and regen `all`, appends 4-entry migration block per class (48 entries, 0
ambiguous). Component index regenerated (42->34 categories, 242->230 core
components); uv.lock re-resolved.

Test fixups:
- Repoint baidu/elastic/searchapi backend tests to lfx_bundles.<provider>.
- test_dynamic_imports (lfx isolated suite) used the now-bundled searchapi as
  its fixture; bundles aren't installed in the engine-only lfx env, so switch
  it to the core langchain_utilities.FakeEmbeddingsComponent (same
  langchain_community optional-import path). 131 shim tests + 21 dynamic-import
  tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* feat(bundles): consolidate homeassistant, olivya, agentql REST providers into lfx-bundles

Third batch (3 providers, 4 components) via consolidate_bundles.py --apply.
These are thin REST wrappers with no dedicated vendor SDK:

- homeassistant (requests) -> lfx_bundles/homeassistant   2 components
- olivya (httpx, lfx core)  -> lfx_bundles/olivya          1 component
- agentql (httpx, lfx core) -> lfx_bundles/agentql         1 component

Moves + shims + per-provider extras + `all` regen; 16 migration entries (0
ambiguous). Component index 34->31 categories, 230->226 components; uv.lock
re-resolved. No tests reference these providers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bundles): consolidate google, vertexai, altk, codeagents into lfx-bundles

Fourth directory-level batch (4 providers, 14 components) via
consolidate_bundles.py. google/vertexai (langchain-google-*), altk
(agent-lifecycle-toolkit), codeagents (smolagents + OpenDsStar); platform/py
markers copied verbatim from langflow-base extras. 56 migration entries (0
ambiguous); index 31->27 categories, 226->212 components. Backend tests
repointed; starter projects embedding moved components regenerated via the
validator. SKIP=detect-secrets: only flags are regenerated code_hash hex
(false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lfx): decouple dynamic-import unit tests from soon-to-be-bundled providers

test_dynamic_imports.py and test_import_utils.py used composio/nvidia/huggingface
as the "lazy category whose SDK is absent in the engine-only lfx env" fixture.
That couples the import-mechanism tests to specific providers — they have broken
each time a provider graduated to a bundle (openai/anthropic/chroma -> composio,
now composio et al.).

Switch to stable in-tree core categories:
- files_and_knowledge.KnowledgeComponent for the "missing-dep" path (its module
  imports langchain_chroma / langflow, absent in bare lfx) — works for both the
  import_mod direct calls and the lfx.components hierarchy access.
- helpers / processing for the "imports cleanly" and "module not found" paths.

No behavior change; 43 tests pass in the isolated lfx env. This unblocks moving
composio, huggingface, nvidia, cuga into lfx-bundles without breaking these tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bundles): consolidate composio, huggingface, nvidia, cuga into lfx-bundles

Fifth batch (4 providers, 71 components) via consolidate_bundles.py --apply,
unblocked by the prior dynamic-import test decoupling:

- composio    -> lfx_bundles/composio    (composio + composio-langchain)  63 components
- huggingface -> lfx_bundles/huggingface (langchain-huggingface[marker], huggingface-hub, community)  2 components
- nvidia      -> lfx_bundles/nvidia      (langchain-nvidia-ai-endpoints, nv-ingest-client[py3.12], gassist[win32])  5 components
- cuga        -> lfx_bundles/cuga        (cuga; platform/py markers)  1 component

Markers copied verbatim from langflow-base extras (uv lock --check clean).
284 migration entries (0 ambiguous); component index 27->23 categories,
212->141 components.

Tests: relocate the dedicated lfx-isolated tests for the now-bundled components
to the backend suite (test_nvidia_component.py, test_cuga_session_id_fallback.py)
repointed to lfx_bundles.<provider>; repoint backend composio/huggingface/cuga
imports + a cuga patch target; pragma a fake test api_key. 43 decoupled lfx
import tests pass post-move. SKIP=detect-secrets: only flags are regenerated
component_index code_hash hex (false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* feat(bundles): consolidate langwatch evaluator into lfx-bundles

langwatch -> lfx_bundles/langwatch (1 component). The LangWatchComponent is a
pure httpx REST wrapper (its only non-core touchpoint is lfx.base.langwatch.utils,
which is also httpx-based) — the langwatch SDK extra in langflow-base is for the
tracing service, not this component, so the bundle declares no extra deps.

Moves + shim + `all` regen; 4 migration entries (0 ambiguous). Component index
23->22 categories, 141->140 components. Backend langwatch test repointed
(16 passed). SKIP=detect-secrets: only flags are regenerated component_index
code_hash hex (false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bundles): consolidate FAISS, Notion into lfx-bundles (lowercase-slug support)

Uppercase source dirs need lowercase bundle slugs (BUNDLE_NAME_RE is
lowercase-only). Enhance consolidate_bundles.py with bundle_slug(provider) =
provider.lower(): the bundle dir, ext id, shim target, and extra key use the
lowercased slug, while the migration import_path keeps the historical
lfx.components.<Provider> casing so saved flows still resolve. Identity for
already-lowercase providers (no change to prior entries).

- FAISS  -> lfx_bundles/faiss  (faiss-cpu[py-split] + langchain-community)  1 component
- Notion -> lfx_bundles/notion (requests + Markdown)  8 components

test_bundle_shims.py: derive the expected bundle name by lowercasing the shim
dir name (FAISS shim aliases lfx_bundles.faiss) — 159 shim tests pass.
Relocate the lfx-isolated FAISS test to backend/vectorstores (repointed to
lfx_bundles.faiss.faiss). 36 migration entries (0 ambiguous); index 22->20
categories, 140->131 components. SKIP=detect-secrets: regenerated code_hash hex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(bundles): fold vectorstores LocalDBComponent into the chroma bundle

The lone LocalDBComponent is Chroma-backed (eager `from langchain_chroma import
Chroma`), so it belongs in the existing `chroma` bundle rather than a new one —
its deps are already covered by the chroma extra (no new deps).

- Move vectorstores/local_db.py -> lfx_bundles/chroma/local_db.py; register
  LocalDBComponent in the chroma bundle's lazy registry.
- Replace lfx.components.vectorstores with a CROSS-BUNDLE shim aliasing
  lfx_bundles.chroma (dir != bundle).
- Migration: lfx.components.vectorstores[.local_db].LocalDBComponent ->
  ext:chroma:LocalDBComponent@official (4 entries, 0 ambiguous).
- test_bundle_shims.py: add a cross-bundle map {vectorstores: chroma} so the
  shim contract validates (161 shim tests pass).
- Repoint the backend LocalDB test (import + patch target) to lfx_bundles.chroma;
  test_all_modules_importable resolves it via the shim.

Component index 20->19 categories, 131->130 components. SKIP=detect-secrets:
regenerated code_hash hex (false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* feat(bundles): remove unbundleable vlmrun + notdiamond components from lfx core

Per the core-tail audit, four providers couldn't be bundled. vlmrun (imports
`from langflow`, violating the lfx engine boundary) and notdiamond (its
`notdiamond` SDK isn't in the lockfile or any extra, so it can't run) have no
engine references, no starter flows, and no migration-table entries — they're
effectively dead in every distribution, so remove them outright.

(crewai and agentics are kept: crewai is woven into 3 starter-project example
flows, and the core Agent hard-depends on agentics.helpers.model_config — both
need deliberate upstream/refactor work, not deletion.)

- Delete src/lfx/.../components/{vlmrun,notdiamond}/ and drop them from the
  lfx.components top-level registry (TYPE_CHECKING / _dynamic_imports / __all__).
- Regenerate component index (19->17 categories, 130->128 components) and backend
  locales (orphan keys removed; extract_backend_strings --check OK).
- Repoint test_dynamic_imports.test_import_error_handling off the deleted
  notdiamond to the core processing category (43 lfx import tests pass).

SKIP=detect-secrets: only flags are regenerated component_index code_hash hex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(lfx): tie flow-builder search 'returns all' to registry size

The hardcoded `count > 100` assertion predated the bundle-extraction
work, which deliberately moves non-legacy components out of lfx core
(now 66 non-legacy of 128 indexed). Assert the empty-query count equals
the full non-legacy registry instead — the durable 'no query == every
component the registry exposes' contract that survives future bundle
moves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(bundles): deterministic component alias resolution + sidebar slug casing after bundle move

Moving agentql/composio and the long tail into lfx-bundles made two
shared-label resolutions order-dependent and left two sidebar slugs
mis-cased, surfacing as red CI on the starter-projects template tests
and the bundles-sidebar e2e.

- flatten_components_with_aliases: register every component's identity
  aliases (registry key / ext class name / name) before any component's
  display_name alias, so a component's own class identity always wins
  its key regardless of registry iteration order. A single setdefault
  pass previously let the Composio 'AgentQL'/'SerpAPI' wrappers
  (display_name) shadow the standalone components (class name) depending
  on load order, so update_starter_projects wrote the wrong
  metadata.module into the AgentQL starter node.
- Correct the AgentQL node module in News Aggregator.json and Price Deal
  Finder.json back to lfx.components.agentql.agentql_api.AgentQL.
- SIDEBAR_BUNDLES: lowercase the Notion/FAISS name slugs to match the
  lowercase bundle category names so both bundles render in the sidebar
  again (display_name and icons unchanged).
- Add order-independence regression tests for the identity/display tiers.

* test(weaviate): patch the class's own module, not a fixed string path

The Weaviate component moved into lfx-bundles, so weaviate.py is now
reachable under several module identities (the lfx.components.weaviate
shim, lfx_bundles.weaviate.weaviate, and the runtime _lfx_ext.* copy).
On Python 3.10, unittest.mock resolves a dotted patch target via a
getattr walk that follows the shim's package alias to a *different*
module object than the one the imported class actually lives in, so
patching 'lfx.components.weaviate.weaviate.WeaviateVectorStore' missed
the object the component body reads -> 'WeaviateVectorStore called 0
times' when an earlier test in the group forced the alternate identity.

Resolve the module from the imported class (sys.modules[Cls.__module__])
and patch.object it directly, bypassing dotted-string resolution. Same
fix for AuthApiKey. Order/identity-independent on all Python versions.

* test(ollama,perplexity): patch the class's own module, not a fixed string path

The Ollama and Perplexity components moved into lfx-bundles, so ollama.py
and perplexity.py are now reachable under several module identities (the
lfx.components.<x>.<x> shim, the canonical lfx_bundles.<x>.<x>, and the
runtime _lfx_ext.* ext-loader copy). On Python 3.10, unittest.mock
resolves a dotted patch target via a getattr walk that follows the shim's
package alias to a *different* module object than the one the imported
class / component body actually use, so patching e.g.
"lfx.components.ollama.ollama.ChatOllama" missed the object the body reads
-> "mock called 0 times" depending on suite order.

Resolve the module from the imported class (sys.modules[Cls.__module__])
and patch.object it directly, bypassing dotted-string resolution:
- perplexity: ChatPerplexity -> patch.object(_PERPLEXITY_MODULE, ...)
- ollama: ChatOllama / logger -> patch.object(_OLLAMA_MODULE, ...)
- ollama: ChatOllamaComponent._parse_json_response ->
  patch.object(ChatOllamaComponent, ...) (the imported class itself)
- ollama: httpx.AsyncClient.get/post -> patched on the global httpx
  (a single class object, identity-independent), dropping the shim prefix

Same fix as the Weaviate test (064069b6bb). Order/identity-independent on
all Python versions; the langchain_ollama / lfx.utils.util / socket
patches target stable modules and are left unchanged.

* fix(locales): restore en.json keys for still-shipping spider/policies components

The spider (SpiderTool) and toolguard (PoliciesComponent) components live in
lfx-bundles and still ship in the palette, but their 44 en.json keys were
pruned while all 6 translated locales (de/es/fr/ja/pt/zh-Hans) retained them.
The extractor walks only lfx.components, so bundle-only components with no
compat shim get dropped from en.json on regeneration. Left as-is, the next GP
upload/download cycle would strip ~264 existing translations (44 keys x 6
langs) for two components that are still in the palette.

Restore the 25 spidertool + 19 policies keys to re-align en.json with the
translated locales. (Durable fix — making the extractor walk src/bundles and/or
adding lfx.components shims for these two providers — is a follow-up, since
spider.py eager-imports its SDK and a shim alone won't make it walkable.)

* chore(deps): drop dead astradb/graph-retriever extras and langchain-aws from langflow-base

After datastax/amazon graduated to lfx-datastax/lfx-amazon, langflow-base still
declared astradb (langchain-astradb), graph-retriever (langchain-graph-retriever,
graph-retriever) and langchain-aws in its aws extra -- all pulled into the
complete aggregate -- though zero core .py files import them (the consuming
components moved to the bundles, which re-declare the same deps). This duplicated
the SDKs in every pip install langflow and was inconsistent with the
openai/anthropic/cohere extras already removed in this PR.

Remove the astradb and graph-retriever extras and their complete refs, and drop
langchain-aws from the aws extra (boto3 stays -- it backs langflow-base's own S3
storage, not just the amazon components). Regenerate uv.lock; closure unchanged,
the SDKs still resolve via lfx-datastax/lfx-amazon.

* ci(extension-migration): enforce component os.environ-write lint in CI

check_component_env_writes.py guards against components writing to process-global
os.environ (which bleeds one caller's credential into other requests on a warm
worker). It was wired only as a pre-commit hook, bypassable via git commit -n,
web-UI commits, or uninstalled hooks. Add it as a stdlib-only job in
extension-migration-checks.yml next to freeze-components, so the credential-bleed
guard is enforced in CI regardless of local pre-commit.

* test(bundles): cover migration-table completeness, preflight provider path, frozen gate

Three test-coverage gaps from the metapackage split, all green on the current tree:
- migration-table completeness: parametrized sweep asserting every lfx_bundles
  provider component class (196 across 72 providers) is reachable via
  migration_table.json as an ext:<provider>:<Class>@ target, so a provider moved
  into a bundle without a table entry fails CI instead of silently breaking saved
  flows. Includes a negative control.
- dependency preflight provider path: drives find_missing_dependencies and
  _preflight_dependencies through a node with a model/agent_llm provider field so
  the _PROVIDER_PACKAGE_FALLBACKS path (the only working provider-dep source in
  engine-only installs) is exercised.
- frozen-components gate: unit tests for check_components_frozen.py covering
  additions (fail), removals (allowed), and the init/pycache/dotdir and
  comment-parsing rules.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix(locales): make spider/toolguard i18n keys survive regen via shims

The earlier restore of the 44 spider/policies en.json keys was reverted by the
gp-backend-check bot: it auto-regenerates en.json on every push to this PR (the
PR-level path filter matches the bundle moves under lfx/components) and the
bundle-blind extractor could not reproduce the keys.

Make them durable the way every other moved provider does:
- Add lfx.components.spider and lfx.components.toolguard '# lfx-bundles-shim'
  packages re-pointing to lfx_bundles.{spider,toolguard}, so the extractor (which
  walks lfx.components) emits SpiderTool/PoliciesComponent keys again.
- Lazy-import spider's SDK (from spider.spider import Spider moved into crawl())
  so SpiderTool is importable for extraction/registration without spider-client,
  matching the lazy pattern weaviate/zep already use (toolguard was already lazy).
- Add spider/toolguard to scripts/ci/frozen_component_dirs.txt so the
  additions-only freeze gate accepts the new shim dirs.
- Restore the 44 keys to en.json.

Verified the extractor now reproduces exactly the 44 keys (25 spidertool + 19
policies), so the bot keeps them instead of dropping them. Bundle components are
dynamic-only (not in component_index.json), so no index regen is needed.

* chore(bundles): declare astrapy in lfx-datastax; align lfx-ibm manifest version

- lfx-datastax imports astrapy directly (astrapy.DataAPIClient/Database in the
  astra components) but only pulled it transitively via langchain-astradb.
  Declare it explicitly, pinned to match langflow-base's astrapy extra
  (>=2.1.0,<3.0.0).
- lfx-ibm's extension.json reported version 0.1.0 while pyproject.toml is 0.1.1;
  the loader uses the manifest version, so bump it to 0.1.1 to match.
Regenerate uv.lock (closure unchanged; astrapy was already resolved
transitively).

* refactor(migrate): guard consolidate against subdir data loss; fix alias docstring

- consolidate_bundles.move_provider copies only top-level *.py but rmtree's the
  whole source tree, so a provider with a subpackage would have its subdir
  silently destroyed and never migration-mapped. Refuse with a clear error
  instead of half-moving (port_bundle.py handles nested layouts). No current
  PROVIDER_DEPS entry has a subdir, so this is a defensive guard.
- component_aliases: the order-independence docstrings overclaimed. Scope the
  guarantee to identity-beats-display (and real-key-beats-alias) and note that
  two components contributing the same identity alias remain first-wins by
  iteration order.

* ci(gp-backend): regen en.json on src/bundles/** edits

The gp-backend-check bot regenerates locales/en.json on PR changes, but its
path filter only watched src/lfx/src/lfx/components/**. Bundle component
strings reach core en.json through the lfx/components/<provider> import shims
(e.g. spider, toolguard), so editing a bundle's display_name/description/info
under src/bundles/** would not re-trigger the extractor and en.json could
silently drift from the translated locales. Add src/bundles/** to the trigger
so bundle-side string edits keep en.json aligned; regen is diff-gated, so
non-string bundle edits remain a no-op.

* fix(bundles): Policies in Models & Agents core; Spider in bundles sidebar

Policies (toolguard) component:
- Move PoliciesComponent + policies/ helpers out of the lfx-bundles
  consolidated metapackage back into lfx/components/models_and_agents core
  (restores the main/release-1.11.0 layout); register it in the category
  __init__ and restore the core-path tests.
- toolguard stays an optional langflow-base extra (langflow-base[toolguard],
  pulled in via [complete]); the component imports it lazily, so a bare lfx
  install still loads/inspects it.
- Remove the lfx-bundles toolguard provider (dir + extra + [all] entry), the
  lfx core import shim, and the frozen_component_dirs.txt entry; drop the 4
  ext:toolguard migration entries; regenerate component_index (128 -> 129)
  and uv.lock.

Spider:
- Add spider to SIDEBAR_BUNDLES so it renders in the Bundles section like
  arXiv instead of falling into the default categories (it was missing from
  the list while peer lfx-bundles providers were present).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(bundles): fix error-tier mirror claim, list-time discovery note, metapackage README

Addresses three documentation nits from the PR #13563 re-review:

- errors.py: the lfx.bundles diagnostics comment claimed a full mirror of the
  inline tier's split, but only the duplicate case matches. name-invalid and
  root-unreadable are warning-only here where their inline counterparts are
  hard errors (ok=False). Tighten the comment to match actual behavior.
- BUNDLE_API.md: note that manifest-less lfx.bundles providers are invisible to
  `lfx extension list` (discover_all_extensions walks only installed + seed;
  load_lfx_bundles_extensions runs in the startup component-loading path), so
  the precedence story isn't read as a list-time property.
- lfx-bundles/README.md: the bulk move has landed on this branch (71 providers,
  populated per-provider extras, generated `all`), so drop the "empty skeleton /
  available later" framing and document the final state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(bundles): surface pip-install guidance for missing SDKs on the server run path

A flow loaded through the UI/API on an engine-only `lfx` (no bundles)
failed deep in the vertex build with a bare
`Error creating class. ModuleNotFoundError(No module named 'langchain_chroma')`.
The actionable install guidance only existed on the `lfx run` preflight
and the legacy import shim; the server run path
(simple_run_flow -> run_graph_internal -> graph.arun()) had neither.

Both vertex-build error formatters (lfx's `format_exception_message`, used
by graph.arun(), and langflow's, used by the build/chat endpoints) now
detect a plain ModuleNotFoundError in the cause chain and attach the same
`pip install <package>` guidance the CLI gives, mapping the missing import
to its PyPI distribution via the existing requirements resolver. Bundle-shim
"components moved to ..." messages are left untouched (their message does
not start with "No module named"), so graduated providers stay actionable.

* fix(bundles): surface curated shim message over its raw cause in exception formatter

get_causing_exception walked to the deepest __cause__, so a graduated
provider's curated "components moved to ..." ModuleNotFoundError (raised
`from` the raw "No module named 'lfx_<x>'") was unwrapped past it. The
formatter then regenerated bare "pip install lfx-<x>" guidance, dropping
the curated text — including the "or pip install langflow, which bundles
it" clause.

Both get_causing_exception copies (lfx + langflow) now stop at the first
ModuleNotFoundError whose message is not a raw "No module named ..." (the
curated shim error), and module_not_found_hint surfaces that curated
message verbatim. Plain SDK import errors are unchanged.

Adds regression tests reproducing the real 3-level cause chain on both
formatters; the old test used a truncated chain that hid this.

* fix(bundles): cover OpenRouter/IBM WatsonX in Orchestrate requirements detection (#13732)

fix(bundles): cover OpenRouter/IBM WatsonX in wxO requirements detection

The bundle split removed the langchain provider SDKs from lfx's own
dependency tree, which makes flow→requirements detection load-bearing for
Watsonx Orchestrate deployments: the runner installs only `lfx` + the
packages `generate_requirements_from_flow` emits, so a provider missing
from the resolution tables yields an empty requirements set and the
deployed flow fails to import its model class.

Two unified-selectable providers were unresolved:
- OpenRouter (runs on ChatOpenAI) was in no resolution table -> [].
- "IBM WatsonX" (catalog casing) only matched the legacy "IBM watsonx.ai"
  key; langchain-ibm landed only by an embedding-path coincidence.

Add both to _PROVIDER_PACKAGE_FALLBACKS (keyed by the exact catalog
strings) so every provider in MODEL_PROVIDER_METADATA resolves.

Tests:
- TestBundleSeparationOrchestrate in test_flow_requirements.py: catalog
  coverage guard, OpenRouter/IBM regressions, and split-contract guards
  (bundle dists and langchain imports must stay out of lfx's tree).
- End-to-end artifact tests in test_watsonx_orchestrate.py asserting a
  bundle-provider flow emits the langchain SDK (not lfx-openai/lfx-bundles)
  and that requirements.txt pins lfx as its first line.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Backend format

* Format again

* [autofix.ci] apply automated fixes

* fix(bundles): pin datastax ruff line-length to 120 to stop format tug-of-war

The datastax bundle's pyproject.toml declares a [tool.ruff.lint.per-file-ignores]
table, which makes ruff treat the directory as a standalone config root. It never
restated line-length, so ruff fell back to its default of 88 there. As a result
`make format_backend` (per-directory config discovery) wrapped lines to 88 while
the pre-commit ruff-format hook (run with --config pyproject.toml, forcing the
repo-wide 120) unwrapped them back, leaving the two formatters fighting forever.

Restate line-length = 120 so both config-resolution paths agree. Use the explicit
line-length rather than extend=../../../pyproject.toml to avoid pulling in the
root lint ruleset, which the bundle intentionally does not enforce.

* fix(bundles): relocate notdiamond + vlmrun into lfx-bundles

Both providers were dropped during the bulk long-tail move with no bundle
and no compatibility shim, removing them from the registry,
component_index, migration table and locales. That breaks saved flows
using the NotDiamond Router or VLM Run Transcription nodes (vlmrun ships
in langflow[complete]), contradicting the zero-user-facing-change goal.

Relocate both the same way as the other long-tail providers:
- move implementations into src/bundles/lfx-bundles/src/lfx_bundles/
- restore lfx.components.{notdiamond,vlmrun} marker shims
- re-register both in lfx/components/__init__.py
- add per-provider extras (vlmrun -> vlmrun[all]) and the 'all' aggregate
- add migration_table targets ext:{notdiamond,vlmrun}:<Class>@official
- restore the 32 en.json strings; refresh uv.lock

* docs(lfx): correct dependency-preflight scope to lfx run only

The preflight is wired into 'lfx run'; 'lfx serve' has its own
module-not-found hint at request time and does not call it.

---------

Co-authored-by: Elif Sema Balcioglu <elifsemabalcioglu@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-23 19:37:10 +00:00
ee659ca38c fix(bundles): floor lfx pin at the minor line's .dev0 so nightlies resolve
The first 1.11.0.dev0 nightly failed its "Test Langflow Main CLI" step:
the fork bump's sync_bundle_lfx_pin.py re-synced every bundle floor to
lfx>=1.11.0, and PEP 440 sorts the nightly's 1.11.0.dev0 BELOW 1.11.0,
so the workspace-built bundles (whose metadata shadows the satisfiable
PyPI lfx-arxiv 0.1.1 during `uv pip install dist/*.whl`) reject the
branch's own lfx while langflow-base pins it exactly — unresolvable.

Floor at lfx>=X.Y.0.dev0,<(X+1).0.0 instead: X.Y.0.dev0 is the lowest
version PEP 440 admits in the minor line, so every devN / rcN / final
satisfies the floor while older lines and the next major stay excluded.
This closes the NIGHTLY.md activation-gate hole structurally — future
minor forks re-sync to a floor their own nightlies already satisfy.

- sync_bundle_lfx_pin.py: lfx_floor_spec emits the .dev0 floor
- port_bundle.py: mirrored _current_lfx_floor kept in step
- bundle pyprojects restamped via the script (arxiv/docling/duckduckgo/ibm)
- test_bundle_lfx_pin.py expectations updated (20/20 passing)
- NIGHTLY.md gate section annotated with the post-activation fix

uv.lock is unaffected (workspace lfx is recorded as an editable source
with no specifier — which is also why uv sync/lock passed in the same
job). The release.yml RC floor-relax sed still matches the new form;
now redundant but harmless. No bundle version bump needed: published
0.1.1 floors >=1.10.0.rc0, satisfiable by the whole 1.11 line.
2026-06-09 18:02:43 -07:00
db8935c233 fix(bundles): sync lfx pin to the 1.10.0 line after version realignment (#13516)
The LFX 0.5.0 -> 1.10.0 realignment (#13176) left every src/bundles/*
package and the port_bundle.py generator flooring lfx>=0.5.0. That
silently permits resolving against the now-dead 0.5.x line: a bundle
built against 1.10.0's BUNDLE_API would ImportError there, and the
BUNDLE_API_VERSION check (both "1") would not catch it. RELEASE.md flags
exactly this — the jump "affects downstream pins, and neither pip nor uv
will flag it."

- Bump the 4 bundle pyprojects (arxiv, docling, duckduckgo, ibm) to
  "lfx>=1.10.0,<2.0.0": floored at the current major.minor line, capped
  below the next lfx major. Fine-grained API compat stays enforced via
  extension.json's lfx.compat against BUNDLE_API_VERSION, not the cap.
- Add scripts/ci/sync_bundle_lfx_pin.py and call it from `make patch` so
  future releases keep bundle floors in step. Idempotent: a no-op on
  patch releases, moves the floor on minor/major bumps. Leaves docling
  self-refs and the nightly lfx-nightly== form untouched.
- port_bundle.py now derives the floor from src/lfx/pyproject.toml, so
  newly-ported bundles are born at the current line.
- Update src/bundles/PORTING.md and add test_bundle_lfx_pin.py (20 tests).
2026-06-04 20:23:36 -07:00
be7edcbda1 chore: unconstrained lfx version for bundles (#13415) 2026-05-29 10:54:38 -07:00
cc009c9133 feat(lfx): Bundle Separation and LFX Extension Framework (#13043)
* feat(lfx): installed-package + seed-directory discovery for production install (LE-1022)

Adds the read-only production install path for Modes A, B, and C of the
Bundle Separation iteration. Manifest-shipping pip-installed
distributions and seed-directory subdirectories are discovered at server
startup and registered as Extensions at @official.

* discovery.py: walks importlib.metadata.distributions() + the
  $LANGFLOW_SEED_DIR / /opt/langflow/bundles seed root; produces
  DiscoveredExtension records and typed errors for malformed manifests
  / configured-but-missing seed dirs.
* registry.py: ExtensionRegistry service with the immutability
  invariant for installed and seed entries. Mutation verbs (uninstall,
  disable, enable, install, update_entry) all raise
  ExtensionImmutableError carrying the typed
  installed-extension-immutable / seed-directory-immutable code so the
  invariant is testable today; the CLI uninstall surface ships in B4.
* lfx extension list: read-only inspector with text and JSON output
  for operators inspecting Mode B/C images.
* Errors: four new typed codes
  (installed-extension-immutable, seed-directory-immutable,
  seed-directory-not-found, duplicate-extension-id) plus snapshot
  coverage in tests/unit/extension/test_errors.py.
* Tests: 165 extension tests pass, including the LE-1022 acceptance
  cases -- three pip-installed wheels visible at @official, three seed
  bundles visible at @official, and the parametrized service-layer
  immutability check across every mutation verb.
* Docs: docs/Deployment/deployment-extensions-production.mdx covers
  the Dockerfile template, k8s deployment notes, the bundle packaging
  convention (extension.json shipped via package-data), and
  troubleshooting for the typed error codes.

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (#12967)

* feat(lfx): add extension manifest schema, validate CLI, and error formatter (LE-1014)

Foundation for the Bundle Separation iteration. Defines what a valid
extension.json looks like (Pydantic models + Draft 2020-12 JSON Schema),
ships the offline `lfx extension validate` command, and ships the single
`format_extension_error` function that every other extension-system module
will use to render structured errors.

What's in lfx.extension:

- `ExtensionManifest` / `BundleRef` / `LangflowCompat` Pydantic models with
  `extra="forbid"` so unknown fields fail loudly. Deferred fields (`services`,
  `routes`, `hooks`, `starter_projects`, `userConfig`) are reserved as
  None-only so non-null values produce a dedicated
  `field-deferred-in-this-milestone` error instead of a generic schema wall.
  `bundles` accepts a list but rejects length > 1 with
  `multi-bundle-deferred-in-this-milestone` (validator-enforced; the loader
  re-checks at install time in LE-1015).
- `schema.build_schema()` + `build_schema_json()` produce the publishable
  artifact at schemas.langflow.org/extension/v1.json.
- `ExtensionError` typed envelope and `format_extension_error` -- one branch
  per discriminant. Codes are registered in `ERROR_CODES`; an
  `ExtensionError` constructed with an unknown code raises at construction
  time, preventing producers from shipping without a matching renderer.
- `validate_extension` runs four passes: manifest discovery + schema,
  path-safety (no `..`, no absolute paths, no symlink escape), AST
  inspection of every `.py` (syntax, Component subclass present, build()
  declared, top-level `import *`, top-level I/O primitives), and an opt-in
  `--execute-imports` that runs each module in a subprocess with a
  temporary HOME / TMPDIR / LANGFLOW_CONFIG_DIR and LANGFLOW_*/LFX_* env
  vars stripped.
- `lfx extension validate` and `lfx extension schema` typer subcommands.

Acceptance-criteria coverage in tests/unit/extension/:

- Round-trips every v0 manifest field; deferred fields rejected; multi-bundle
  rejected with the dedicated discriminant.
- JSON Schema validates the v0 example and rejects 12 malformed manifests
  with distinct error paths (>= 10 required by the ticket).
- Median default-validate runtime < 100ms on the basic template.
- Crafted side-effect bundle: default validate does NOT execute it (canary
  file is never written); `--execute-imports` DOES execute it and the canary
  appears, while LANGFLOW_* env vars are NOT inherited by the subprocess.
- Snapshot tests for every code in ERROR_CODES; a guard test verifies
  ERROR_CODES and the snapshot table are in lockstep so future additions
  cannot ship without a format branch and a snapshot.

Wiring: `lfx extension` is a sub-app under the Authoring help panel so
future tickets (LE-1016 init/dev, LE-1018 reload) can attach without
colliding with the existing `lfx validate` (which validates flow JSON, not
extensions).

* fix(lfx): fall back to tomli on Python 3.10 in extension manifest loader

tomllib is stdlib only on 3.11+, but lfx supports 3.10-3.13. Use the
existing tomli runtime dependency as the 3.10 fallback (same API, so the
import alias keeps the rest of the module unchanged).

Fixes ModuleNotFoundError seen in CI on the 3.10 job.

* Update src/lfx/tests/unit/extension/test_schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/cli/_extension_commands.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (LE-1015)

Introduces lfx.extension.loader: the runtime that turns an Extension on disk
into LoadedComponent records keyed by ext:<bundle>:<Class>@<slot>. Two paths in:

  - load_extension(root): one manifest, one Bundle, registered at @official.
    Re-checks multi-bundle at runtime (defense-in-depth vs. the schema).
  - discover_inline_bundles(paths): each subfolder of LANGFLOW_COMPONENTS_PATH
    is a Bundle at @extra. Walk order is platform-independent (sorted dirs,
    user-declared path order). First-wins on duplicate names; second emits
    duplicate-inline-bundle warning that names both paths.

Manifest-first precedence helpers (installed_extension_roots,
manifest_owning_distributions, filter_plugin_entry_points) let callers of
the legacy langflow.plugins entry-point loader skip distributions that ship
a manifest, so component entry-points are not double-registered.

New typed error codes: module-import-failed, duplicate-component-name,
duplicate-distribution, duplicate-inline-bundle, inline-bundle-name-invalid.
Each ships with a format branch and a snapshot test.

Tests cover the AC: single-bundle happy path, multi-bundle rejection,
missing/empty/no-Component bundle, duplicate class names, deterministic
walk order, recursive discovery, inline-bundle first-wins + dot-dir skip
+ bundle.json metadata, manifest-first precedence partition, PEP-503
distribution-name canonicalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1015 review feedback

- Drop the unproduced duplicate-distribution error code; LE-1022 will add
  it once startup-time discovery has a place to surface it.  Restores the
  invariant that every code in ERROR_CODES has a producer.
- Clarify that intra-bundle relative imports are NOT supported in v0; only
  absolute references between bundle modules work in this milestone.
- Use strict=False when re-resolving bundle_root in the walker; the path
  was already existence-checked, and a concurrent removal in the narrow
  window should not raise across the loader's public boundary.
- Hoist the json import out of _read_inline_bundle_json's body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(lfx): split extension loader into a small subpackage (LE-1015)

Addresses the file-size feedback from the LE-1015 review.  The single
870-line loader.py becomes a flat package keyed off the four section
banners that already existed inline:

    loader/
      __init__.py     # re-exports the public surface
      _types.py       # SLOT constants, LoadedComponent, LoadResult
      _discovery.py   # filesystem walk + importlib.util orchestration
      _detection.py   # Component subclass identification (MRO heuristic)
      _orchestrator.py # load_extension, discover_inline_bundles
      _plugins.py     # manifest-first precedence over langflow.plugins

Largest file is now _orchestrator.py at 440 LOC (was 870); every file is
well under the 800-LOC project guideline.  No behavior change: the public
import paths from lfx.extension are unchanged, all 38 loader tests still
pass.  ``_canonicalize_distribution`` is now exported as
``canonicalize_distribution`` from ``loader._plugins`` (it's a stable
PEP-503 helper that downstream modules will reach for, so it loses the
private underscore).

The test suite is split to mirror the package:

    tests/unit/extension/loader/
      conftest.py             # shared fixtures, FakeDist, autouse scrub
      test_load_extension.py  # @official slot, identity, failure modes
      test_inline_bundles.py  # LANGFLOW_COMPONENTS_PATH, @extra slot
      test_plugins.py         # manifest-first precedence helpers
      test_types.py           # LoadedComponent, LoadResult, code parity

Each test file imports its own slice of the public API and pulls fixtures
from conftest, so a reader looking at one banner can read it in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: LangflowCompat -> LfxCompat

* fix: Remove references to ticket

* fix: encode maxItems constraint in schema

* fix: deferred fields and schema version

* Fix ruff errors

* fix: align loader tests with renamed manifest API and strip ticket refs

The merge of feat/extension-validate brought in the LfxCompat / compat
rename, but the loader test fixtures still used LangflowCompat / bundle_api
and failed at the manifest layer. Update conftest.py + test_load_extension.py
to the post-rename API.

Strip 17 LE-XXXX ticket references from loader source files, errors.py
loader-specific comments, and the four loader test docstrings, matching the
convention applied to LE-1014. Descriptive prose preserved.

Promote _BUNDLE_NAME_RE to BUNDLE_NAME_RE on the manifest module so the
loader's orchestrator no longer reaches across modules into a private name.

* refactor(lfx): clarify broad except, log malformed bundle.json, expand test docstring

- Document why _discovery.import_bundle_module catches BaseException (startup-time loader, must surface bad bundles as typed errors rather than abort).
- Add debug-level logging when bundle.json is malformed or non-object so a stale-cache footgun is at least observable.
- Expand test_skips_re_imported_class docstring so future maintainers don't accidentally weaken the __module__-equality guard if package-style relative imports get added later.

Also drops the stale duplicate-distribution claim from the PR description; that code is correctly deferred to LE-1022 along with /all integration.

* feat(lfx): wire Extension System into /all, pathsep-split LANGFLOW_COMPONENTS_PATH, emit duplicate-distribution

Closes the four AC gaps the previous reviewer flagged on PR #12967:

1. /all integration: get_and_cache_all_types_dict now also calls a new
   import_extension_components() that loads installed Extensions via
   load_installed_extensions, loads inline bundles via discover_inline_bundles,
   and builds frontend-node templates with extension/bundle/extension_version
   fields stamped on. Failures are logged and skipped per bundle.

2. LANGFLOW_COMPONENTS_PATH is now split on os.pathsep so multi-entry env vars
   (e.g. /a:/b on POSIX) produce multiple components-path entries instead of
   one literal non-existent path. Empty segments and missing paths are skipped.

3. duplicate-distribution is a real producer: load_installed_extensions
   surfaces a typed warning on the winner LoadResult when two distributions
   share a canonical name, naming every involved manifest path.

4. Manifest-first precedence runtime wiring: new filter_component_entry_points
   loads each entry-point and only skips ones that resolve to a Component
   subclass on a manifest-shipping distribution. plugin_routes.load_plugin_routes
   now applies it so non-component entry-points (route registrars) keep
   loading per the AC's 'unaffected' promise. Added the previously-missing
   AC test for same-distribution component+non-component partition.

Also makes _distribution_canonical_name defensive against MagicMock test seams.

* fix(lfx): register extension components under namespaced ID; promote duplicate-distribution to error

Addresses the latest review of PR #12967:

P1: /all integration now keys the cache inner dict by LoadedComponent.namespaced_id
(ext:<bundle>:<Class>@<slot>) rather than the bare class name. Templates also carry
the namespaced_id as an explicit field so consumers that look at the value (not the
key) still see the canonical address. This is the form the LE-1020 migration table
will rewrite legacy class-name references to.

P2: load_installed_extensions now appends duplicate-distribution to result.errors
instead of result.warnings, so LoadResult.ok=False when two distributions share a
canonical name. The winner's components still appear in result.components so flows
already pinned to them keep working; only the conflict status changes. Updated test
to assert errors + ok=False; added explicit assertion that the winner's components
are still present.

* fix(lfx): installed-distribution discovery accepts pyproject.toml manifest form

Closes the latest review finding on PR #12967: the installed-distribution
scan only looked for extension.json, ignoring distributions whose manifest
lives in [tool.langflow.extension] inside pyproject.toml. The AC explicitly
treats both as valid manifest forms.

_distribution_manifest_path now:
- Returns extension.json immediately when present (preserves precedence
  matching load_manifest's discovery order).
- Falls back to pyproject.toml only when extension.json is absent AND the
  pyproject's [tool.langflow.extension] section is parseable.

Validation reuses load_manifest itself so the rule lives in exactly one
place; a stray pyproject.toml without the section is correctly ignored.

Tests cover: pyproject-only discovery, pyproject-without-section ignored,
extension.json wins on collision, end-to-end pyproject load at @official,
and pyproject-form manifest-first entry-point suppression.

* refactor(lfx): tighten loader invariants, surface silent skips, harden tests

Addresses the latest review feedback on PR #12967:

Type-level invariants (_types.py):
- LoadedComponent.__post_init__ enforces that @extra components must NOT
  carry a distribution. The reverse (@official without distribution) is
  permitted because load_extension is also used for dev-mode loads
  against a working tree before pip install.
- LoadResult docstring documents the partial-success contract: components
  may be non-empty when errors is non-empty (some files imported, others
  failed). Callers branching on ok get strict success.

Silent-failure fixes (_orchestrator.py + settings/base.py):
- inline-path-missing: a non-existent / non-dir LANGFLOW_COMPONENTS_PATH
  entry now produces a typed warning per skipped path so a typo no
  longer yields zero diagnostics. Settings-layer skip bumped from debug
  to warning for the same reason.
- bundle-json-invalid: a malformed or non-object bundle.json now surfaces
  a typed warning instead of silently rewriting the user-declared
  id/version to derived values under the same bundle name.
- no-component-subclass gating uses a call-local counter instead of
  result.errors so the diagnostic stays accurate when a future caller
  reuses a LoadResult (multi-bundle / batch wrapper scenarios).

Test hardening:
- test_re_imported_class_is_skipped_via_module_filter rewritten to
  actually exercise the __module__-equality check via sys.modules
  injection; previously passed via module-import-failed (relative-import
  failure), which would silently weaken if package registration changes.
- test_user_declared_path_order_is_preserved: AC #8's multi-path order
  case (distinct bundles in [path_b, path_a]) was unasserted; added.
- test_inline_module_import_failure_attributes_identity: AC #10's
  identity-on-partial-failure was covered for @official but not @extra;
  added.
- test_uses_real_distributions_by_default tightened to assert ep
  placement (in kept, not in skipped) instead of exact-list equality, so
  a future Langflow-shipped manifest doesn't silently flip the assertion.

bumped 64 -> 175 passing extension tests; 20 backend integration tests
still pass.

* fix(lfx): malformed pyproject manifests surface manifest-invalid instead of disappearing

Closes the latest review finding on PR #12967: a pyproject.toml with a
[tool.langflow.extension] section that has missing/invalid required
fields was silently dropped because _pyproject_has_extension_section
ran full schema validation via load_manifest and returned False on
ValueError/TypeError. That conflated 'no section' with 'section
malformed'.

Fix: detect section presence only. _pyproject_has_extension_section now
calls _read_pyproject_extension (TOML parse + key lookup, no schema
check). Behavior:
- Section absent or pyproject TOML unparseable -> False (treat as
  regular non-manifest package).
- Section present and is a table (valid OR schema-invalid) -> True.
- Section present but is not a table -> True; the author intended to
  declare an extension and load_extension will surface the typed error.

This way a typo'd pyproject Extension produces a typed manifest-invalid
LoadResult with extension_id attribution, and manifest-first precedence
still suppresses its legacy component entry-points -- matching the
'typed load results on success/failure' contract for the supported
pyproject manifest form.

Tests: two new cases pin the behavior. test_malformed_pyproject_section_
surfaces_manifest_invalid asserts a typed load-failure result with
distribution attribution; the second test pins manifest-first
suppression for malformed pyproject distributions.

* refactor(lfx): emit inline-path-unreadable, document reload contract, trim rot

Closes the remaining nits on PR #12967:

- inline-path-unreadable (new typed error code): a configured
  LANGFLOW_COMPONENTS_PATH entry that raises OSError on iterdir
  (typically permission-denied) now produces a typed LoadResult error
  carrying str(exc) instead of silently swallowing the message.
- duplicate-inline-bundle hint trimmed: removed forward promise about
  hard-error-in-a-later-release; same actionability, no expiration.
- discover_inline_bundles docstring trimmed from three paragraphs to
  two sentences (per CLAUDE.md anti-multi-paragraph rule).
- _distribution_manifest_path docstring trimmed to one-liner.
- installed_extension_roots dropped Used-by caller-narration list;
  manifest_owning_distributions docstring rewritten to call out the
  shadow-load risk for direct callers and point to load_installed_
  extensions for the typed warning surface.
- _discovery.import_bundle_module: replaced 'until that lands in a
  later milestone' rot with a single line ('Absolute imports only
  between bundle modules; relative imports unsupported.') AND added
  the single-load-per-process contract note documenting why LE-1018
  reload must scrub registry/sys.modules before re-invoking the loader.
- load_extension docstring grew a 'Single-load-per-process contract'
  block telling direct callers not to rely on this function for refresh.

Tests: new test_unreadable_path_emits_inline_path_unreadable pins the
OSError -> typed-error path.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(lfx): atomic-swap Bundle reload pipeline + endpoint + CLI (LE-1018) (#12979)

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (LE-1015)

Introduces lfx.extension.loader: the runtime that turns an Extension on disk
into LoadedComponent records keyed by ext:<bundle>:<Class>@<slot>. Two paths in:

  - load_extension(root): one manifest, one Bundle, registered at @official.
    Re-checks multi-bundle at runtime (defense-in-depth vs. the schema).
  - discover_inline_bundles(paths): each subfolder of LANGFLOW_COMPONENTS_PATH
    is a Bundle at @extra. Walk order is platform-independent (sorted dirs,
    user-declared path order). First-wins on duplicate names; second emits
    duplicate-inline-bundle warning that names both paths.

Manifest-first precedence helpers (installed_extension_roots,
manifest_owning_distributions, filter_plugin_entry_points) let callers of
the legacy langflow.plugins entry-point loader skip distributions that ship
a manifest, so component entry-points are not double-registered.

New typed error codes: module-import-failed, duplicate-component-name,
duplicate-distribution, duplicate-inline-bundle, inline-bundle-name-invalid.
Each ships with a format branch and a snapshot test.

Tests cover the AC: single-bundle happy path, multi-bundle rejection,
missing/empty/no-Component bundle, duplicate class names, deterministic
walk order, recursive discovery, inline-bundle first-wins + dot-dir skip
+ bundle.json metadata, manifest-first precedence partition, PEP-503
distribution-name canonicalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1015 review feedback

- Drop the unproduced duplicate-distribution error code; LE-1022 will add
  it once startup-time discovery has a place to surface it.  Restores the
  invariant that every code in ERROR_CODES has a producer.
- Clarify that intra-bundle relative imports are NOT supported in v0; only
  absolute references between bundle modules work in this milestone.
- Use strict=False when re-resolving bundle_root in the walker; the path
  was already existence-checked, and a concurrent removal in the narrow
  window should not raise across the loader's public boundary.
- Hoist the json import out of _read_inline_bundle_json's body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(lfx): split extension loader into a small subpackage (LE-1015)

Addresses the file-size feedback from the LE-1015 review.  The single
870-line loader.py becomes a flat package keyed off the four section
banners that already existed inline:

    loader/
      __init__.py     # re-exports the public surface
      _types.py       # SLOT constants, LoadedComponent, LoadResult
      _discovery.py   # filesystem walk + importlib.util orchestration
      _detection.py   # Component subclass identification (MRO heuristic)
      _orchestrator.py # load_extension, discover_inline_bundles
      _plugins.py     # manifest-first precedence over langflow.plugins

Largest file is now _orchestrator.py at 440 LOC (was 870); every file is
well under the 800-LOC project guideline.  No behavior change: the public
import paths from lfx.extension are unchanged, all 38 loader tests still
pass.  ``_canonicalize_distribution`` is now exported as
``canonicalize_distribution`` from ``loader._plugins`` (it's a stable
PEP-503 helper that downstream modules will reach for, so it loses the
private underscore).

The test suite is split to mirror the package:

    tests/unit/extension/loader/
      conftest.py             # shared fixtures, FakeDist, autouse scrub
      test_load_extension.py  # @official slot, identity, failure modes
      test_inline_bundles.py  # LANGFLOW_COMPONENTS_PATH, @extra slot
      test_plugins.py         # manifest-first precedence helpers
      test_types.py           # LoadedComponent, LoadResult, code parity

Each test file imports its own slice of the public API and pulls fixtures
from conftest, so a reader looking at one banner can read it in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(lfx): atomic-swap Bundle reload pipeline + endpoint + CLI (LE-1018)

Five-stage reload (parallel staging load -> validate -> swap under write
lock -> cleanup -> emit) for installed Bundles in Mode A.  In-flight
flows keep the pre-swap class via existing references; new flows pick up
the post-swap class atomically; concurrent reloads on the same Bundle
are rejected with reload-in-progress.

Adds:

* lfx/extension/registry.py -- BundleRegistry with per-bundle
  reload-in-progress guard and components_index.json writer
* lfx/extension/reload.py -- the five-stage pipeline; events emission
  is stubbed (TODO LE-1017) so the swap mechanics can ship before the
  events service lands
* loader: optional module_namespace param so Stage 1 lands in
  __reload_staging__.<id> instead of the live _lfx_ext.* namespace
* errors: four new typed reload codes (reload-in-progress,
  reload-bundle-not-installed, reload-bundle-name-mismatch,
  reload-source-missing) with branch templates and snapshot tests
* HTTP: POST /api/v1/extensions/{id}/bundles/{name}/reload, gated by
  the existing get_current_active_user dependency, returns 409 with a
  typed body for the in-progress collision case
* CLI: lfx extension reload <id> [--bundle <name>] -- HTTP client
  against the dev server with text/json output and proper exit codes
  (--all is gated until LE-1019 lands the list endpoint)
* tests: 16 reload-pipeline tests covering the AC matrix (rename
  round-trip, broken-bundle isolation, concurrent readers, in-flight
  flow, double-reload guard, bundle-name mismatch) plus 9 CLI client
  tests

Mode A only.  In Mode B/C bundle changes require a Docker image rebuild
and the reload path is not exercised.

The events emission in Stage 5 is intentionally stubbed -- the LE-1017
ticket will swap the body of _emit_bundle_reload_event in one place
without touching the pipeline core.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* feat(lfx): add `extension init` and `extension dev` CLIs (LE-1016) (#12968)

* feat(lfx): add extension manifest schema, validate CLI, and error formatter (LE-1014)

Foundation for the Bundle Separation iteration. Defines what a valid
extension.json looks like (Pydantic models + Draft 2020-12 JSON Schema),
ships the offline `lfx extension validate` command, and ships the single
`format_extension_error` function that every other extension-system module
will use to render structured errors.

What's in lfx.extension:

- `ExtensionManifest` / `BundleRef` / `LangflowCompat` Pydantic models with
  `extra="forbid"` so unknown fields fail loudly. Deferred fields (`services`,
  `routes`, `hooks`, `starter_projects`, `userConfig`) are reserved as
  None-only so non-null values produce a dedicated
  `field-deferred-in-this-milestone` error instead of a generic schema wall.
  `bundles` accepts a list but rejects length > 1 with
  `multi-bundle-deferred-in-this-milestone` (validator-enforced; the loader
  re-checks at install time in LE-1015).
- `schema.build_schema()` + `build_schema_json()` produce the publishable
  artifact at schemas.langflow.org/extension/v1.json.
- `ExtensionError` typed envelope and `format_extension_error` -- one branch
  per discriminant. Codes are registered in `ERROR_CODES`; an
  `ExtensionError` constructed with an unknown code raises at construction
  time, preventing producers from shipping without a matching renderer.
- `validate_extension` runs four passes: manifest discovery + schema,
  path-safety (no `..`, no absolute paths, no symlink escape), AST
  inspection of every `.py` (syntax, Component subclass present, build()
  declared, top-level `import *`, top-level I/O primitives), and an opt-in
  `--execute-imports` that runs each module in a subprocess with a
  temporary HOME / TMPDIR / LANGFLOW_CONFIG_DIR and LANGFLOW_*/LFX_* env
  vars stripped.
- `lfx extension validate` and `lfx extension schema` typer subcommands.

Acceptance-criteria coverage in tests/unit/extension/:

- Round-trips every v0 manifest field; deferred fields rejected; multi-bundle
  rejected with the dedicated discriminant.
- JSON Schema validates the v0 example and rejects 12 malformed manifests
  with distinct error paths (>= 10 required by the ticket).
- Median default-validate runtime < 100ms on the basic template.
- Crafted side-effect bundle: default validate does NOT execute it (canary
  file is never written); `--execute-imports` DOES execute it and the canary
  appears, while LANGFLOW_* env vars are NOT inherited by the subprocess.
- Snapshot tests for every code in ERROR_CODES; a guard test verifies
  ERROR_CODES and the snapshot table are in lockstep so future additions
  cannot ship without a format branch and a snapshot.

Wiring: `lfx extension` is a sub-app under the Authoring help panel so
future tickets (LE-1016 init/dev, LE-1018 reload) can attach without
colliding with the existing `lfx validate` (which validates flow JSON, not
extensions).

* fix(lfx): fall back to tomli on Python 3.10 in extension manifest loader

tomllib is stdlib only on 3.11+, but lfx supports 3.10-3.13. Use the
existing tomli runtime dependency as the 3.10 fallback (same API, so the
import alias keeps the rest of the module unchanged).

Fixes ModuleNotFoundError seen in CI on the 3.10 job.

* Update src/lfx/tests/unit/extension/test_schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/cli/_extension_commands.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (LE-1015)

Introduces lfx.extension.loader: the runtime that turns an Extension on disk
into LoadedComponent records keyed by ext:<bundle>:<Class>@<slot>. Two paths in:

  - load_extension(root): one manifest, one Bundle, registered at @official.
    Re-checks multi-bundle at runtime (defense-in-depth vs. the schema).
  - discover_inline_bundles(paths): each subfolder of LANGFLOW_COMPONENTS_PATH
    is a Bundle at @extra. Walk order is platform-independent (sorted dirs,
    user-declared path order). First-wins on duplicate names; second emits
    duplicate-inline-bundle warning that names both paths.

Manifest-first precedence helpers (installed_extension_roots,
manifest_owning_distributions, filter_plugin_entry_points) let callers of
the legacy langflow.plugins entry-point loader skip distributions that ship
a manifest, so component entry-points are not double-registered.

New typed error codes: module-import-failed, duplicate-component-name,
duplicate-distribution, duplicate-inline-bundle, inline-bundle-name-invalid.
Each ships with a format branch and a snapshot test.

Tests cover the AC: single-bundle happy path, multi-bundle rejection,
missing/empty/no-Component bundle, duplicate class names, deterministic
walk order, recursive discovery, inline-bundle first-wins + dot-dir skip
+ bundle.json metadata, manifest-first precedence partition, PEP-503
distribution-name canonicalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1015 review feedback

- Drop the unproduced duplicate-distribution error code; LE-1022 will add
  it once startup-time discovery has a place to surface it.  Restores the
  invariant that every code in ERROR_CODES has a producer.
- Clarify that intra-bundle relative imports are NOT supported in v0; only
  absolute references between bundle modules work in this milestone.
- Use strict=False when re-resolving bundle_root in the walker; the path
  was already existence-checked, and a concurrent removal in the narrow
  window should not raise across the loader's public boundary.
- Hoist the json import out of _read_inline_bundle_json's body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(lfx): split extension loader into a small subpackage (LE-1015)

Addresses the file-size feedback from the LE-1015 review.  The single
870-line loader.py becomes a flat package keyed off the four section
banners that already existed inline:

    loader/
      __init__.py     # re-exports the public surface
      _types.py       # SLOT constants, LoadedComponent, LoadResult
      _discovery.py   # filesystem walk + importlib.util orchestration
      _detection.py   # Component subclass identification (MRO heuristic)
      _orchestrator.py # load_extension, discover_inline_bundles
      _plugins.py     # manifest-first precedence over langflow.plugins

Largest file is now _orchestrator.py at 440 LOC (was 870); every file is
well under the 800-LOC project guideline.  No behavior change: the public
import paths from lfx.extension are unchanged, all 38 loader tests still
pass.  ``_canonicalize_distribution`` is now exported as
``canonicalize_distribution`` from ``loader._plugins`` (it's a stable
PEP-503 helper that downstream modules will reach for, so it loses the
private underscore).

The test suite is split to mirror the package:

    tests/unit/extension/loader/
      conftest.py             # shared fixtures, FakeDist, autouse scrub
      test_load_extension.py  # @official slot, identity, failure modes
      test_inline_bundles.py  # LANGFLOW_COMPONENTS_PATH, @extra slot
      test_plugins.py         # manifest-first precedence helpers
      test_types.py           # LoadedComponent, LoadResult, code parity

Each test file imports its own slice of the public API and pulls fixtures
from conftest, so a reader looking at one banner can read it in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(lfx): add `extension init` and `extension dev` CLIs (LE-1016)

The two scaffolding CLIs an Extension author types:

  - `lfx extension init <target>` writes the basic single-Bundle
    template (manifest with $schema, README, .gitignore, one Component
    subclass + a pytest smoke test).  AC #1: the generated extension
    validates clean against LE-1014.  AC #2: the generated test file is
    a valid pytest module that exercises the component's build() method.
    AC #3: any --template other than 'basic' fails with a typed
    template-deferred-in-this-milestone error and a non-zero exit.
    Refuses to scaffold over a non-empty target dir.

  - `lfx extension dev <target>` validates the local extension, records
    its absolute path in <config_dir>/extensions/dev_extensions.json,
    prints reload instructions, and execs `langflow run` (or
    `python -m langflow` when langflow isn't on PATH).  --skip-launch
    registers without launching (used by tests + external dev-server
    scripts); --skip-validate lets authors register a known-broken
    manifest to debug it under the loader.

Stack:
  - Wave 0: error codes (extension-target-exists,
    extension-target-invalid, local-extension-missing) with format
    branches and snapshot tests.
  - Wave 1: lfx.extension.init_template -- pure-data scaffolder, no
    Typer dependency so the CLI is a thin shell over it.
  - Wave 1: lfx.extension.dev_registry -- atomic JSON state file under
    the langflow user-cache dir; helpers for register / list /
    unregister / load_dev_extensions / dev_extension_component_paths.
  - Wave 2: lfx.cli._extension_commands gains init/dev subcommands.
  - Wave 2: langflow.main lifespan hook reads the dev registry after
    bundle loading and extends components_path with each registered
    bundle dir, so the existing palette discovery picks up dev
    extensions.  Missing paths surface as local-extension-missing
    warnings (AC #5) without aborting startup.

Tests (84 new, 197 total in tests/unit/extension/):
  - test_init_template.py: AC scenarios + identifier derivation +
    deterministic file shape.
  - test_dev_registry.py: register/list/unregister round-trip,
    idempotent re-register refreshes timestamp, malformed state file
    treated as empty, missing-path warning, recovery when path
    reappears, env-var override precedence.
  - test_cli.py: AC #1 init->validate, AC #3 deferred templates,
    --skip-launch registers without launching, --skip-validate
    short-circuits the pre-flight pass.
  - test_errors.py: snapshot rows for all three new codes; the
    every-known-code-has-a-snapshot test enforces parity.

LE-1018 (reload) reuses load_dev_extensions; LE-1022 (installed-pkg
discovery) shares the components_path extension pattern.  AC #4 ("boots
Langflow with the extension visible in the palette within 5s") is
delivered jointly by `extension dev` (registers + execs) and the
lifespan hook (loads on startup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1016 review feedback

Fixes the four HIGH issues + the elevated LOW from the review pass:

1. dev_extension_component_paths now forwards EVERY warning, not just
   local-extension-missing.  Previously a duplicate-component-name (or
   any future warning code) was silently dropped, hiding real signal
   from the lifespan hook's logs.

2. Defensive emit when a LoadResult has components but source_path is
   None.  The current loader always sets source_path, but a future
   hand-built LoadResult could violate that contract; we now surface a
   typed local-extension-missing error rather than dropping the
   extension silently.

3. Replaced the fragile ``min(len(parts))`` bundle-root selection with
   a relative-to-source-path measurement.  Handles deep-vs-shallow
   sibling extensions correctly without depending on absolute path
   depth.

4. Generated README now documents the langflow/lfx prerequisite under
   the Develop section so authors know `pytest` requires the lfx
   environment, not just Python.

5. Forced LANGFLOW_LAZY_LOAD_COMPONENTS=false unconditionally in the
   `extension dev` exec env (was setdefault, which let a developer's
   global lazy-loading export silently hide their dev components from
   the palette and miss AC #4's 5s budget).

Plus three MEDIUMs:

  - sys.modules cleanup in test_generated_test_file_runs_against_generated_component
    so a later test importing the same dotted path doesn't pick up a
    stale module from a deleted tmp_path.  Component instantiation
    moved inside the try block because Component.__init__ uses
    inspect.getsourcefile against self.__class__'s still-live module.
  - Added regex-drift test that pins the init_template patterns to
    match manifest.py's so a schema regex change can't quietly produce
    invalid scaffolded manifests.
  - Added two new dev_registry tests covering the forward-all-warnings
    contract and the source_path=None defense.

Tests: 201 passing (4 new); ruff check + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: LangflowCompat -> LfxCompat

* fix: Remove references to ticket

* fix: encode maxItems constraint in schema

* fix: deferred fields and schema version

* Fix ruff errors

* fix: align loader tests with renamed manifest API and strip ticket refs

The merge of feat/extension-validate brought in the LfxCompat / compat
rename, but the loader test fixtures still used LangflowCompat / bundle_api
and failed at the manifest layer. Update conftest.py + test_load_extension.py
to the post-rename API.

Strip 17 LE-XXXX ticket references from loader source files, errors.py
loader-specific comments, and the four loader test docstrings, matching the
convention applied to LE-1014. Descriptive prose preserved.

Promote _BUNDLE_NAME_RE to BUNDLE_NAME_RE on the manifest module so the
loader's orchestrator no longer reaches across modules into a private name.

* fix(lfx): align init template with renamed manifest API

After merging feat/extension-loader into this branch, two surfaces
fell out of sync with the renamed manifest schema:

- init_template generates extension.json with `lfx: {bundle_api: [1]}`,
  but the validator now requires `lfx: {compat: ["1"]}` per LfxCompat.
  This made `test_basic_template_validates_clean` fail with
  manifest-invalid (`lfx.compat: Field required; lfx.bundle_api: Extra
  inputs are not permitted`).
- test_init_template_regexes_match_manifest_schema reads
  `manifest_mod._BUNDLE_NAME_RE`, but that symbol was promoted to public
  `BUNDLE_NAME_RE` in c63f84a591. Update the test to reference the
  public name; init_template's local copy is still private since it
  also covers `_EXTENSION_ID_RE`, which remains private upstream.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(lfx): append-only migration table + flow deserializer rewrite hook (#13024)

* feat(lfx): add extension manifest schema, validate CLI, and error formatter (LE-1014)

Foundation for the Bundle Separation iteration. Defines what a valid
extension.json looks like (Pydantic models + Draft 2020-12 JSON Schema),
ships the offline `lfx extension validate` command, and ships the single
`format_extension_error` function that every other extension-system module
will use to render structured errors.

What's in lfx.extension:

- `ExtensionManifest` / `BundleRef` / `LangflowCompat` Pydantic models with
  `extra="forbid"` so unknown fields fail loudly. Deferred fields (`services`,
  `routes`, `hooks`, `starter_projects`, `userConfig`) are reserved as
  None-only so non-null values produce a dedicated
  `field-deferred-in-this-milestone` error instead of a generic schema wall.
  `bundles` accepts a list but rejects length > 1 with
  `multi-bundle-deferred-in-this-milestone` (validator-enforced; the loader
  re-checks at install time in LE-1015).
- `schema.build_schema()` + `build_schema_json()` produce the publishable
  artifact at schemas.langflow.org/extension/v1.json.
- `ExtensionError` typed envelope and `format_extension_error` -- one branch
  per discriminant. Codes are registered in `ERROR_CODES`; an
  `ExtensionError` constructed with an unknown code raises at construction
  time, preventing producers from shipping without a matching renderer.
- `validate_extension` runs four passes: manifest discovery + schema,
  path-safety (no `..`, no absolute paths, no symlink escape), AST
  inspection of every `.py` (syntax, Component subclass present, build()
  declared, top-level `import *`, top-level I/O primitives), and an opt-in
  `--execute-imports` that runs each module in a subprocess with a
  temporary HOME / TMPDIR / LANGFLOW_CONFIG_DIR and LANGFLOW_*/LFX_* env
  vars stripped.
- `lfx extension validate` and `lfx extension schema` typer subcommands.

Acceptance-criteria coverage in tests/unit/extension/:

- Round-trips every v0 manifest field; deferred fields rejected; multi-bundle
  rejected with the dedicated discriminant.
- JSON Schema validates the v0 example and rejects 12 malformed manifests
  with distinct error paths (>= 10 required by the ticket).
- Median default-validate runtime < 100ms on the basic template.
- Crafted side-effect bundle: default validate does NOT execute it (canary
  file is never written); `--execute-imports` DOES execute it and the canary
  appears, while LANGFLOW_* env vars are NOT inherited by the subprocess.
- Snapshot tests for every code in ERROR_CODES; a guard test verifies
  ERROR_CODES and the snapshot table are in lockstep so future additions
  cannot ship without a format branch and a snapshot.

Wiring: `lfx extension` is a sub-app under the Authoring help panel so
future tickets (LE-1016 init/dev, LE-1018 reload) can attach without
colliding with the existing `lfx validate` (which validates flow JSON, not
extensions).

* fix(lfx): fall back to tomli on Python 3.10 in extension manifest loader

tomllib is stdlib only on 3.11+, but lfx supports 3.10-3.13. Use the
existing tomli runtime dependency as the 3.10 fallback (same API, so the
import alias keeps the rest of the module unchanged).

Fixes ModuleNotFoundError seen in CI on the 3.10 job.

* Update src/lfx/tests/unit/extension/test_schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/cli/_extension_commands.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (LE-1015)

Introduces lfx.extension.loader: the runtime that turns an Extension on disk
into LoadedComponent records keyed by ext:<bundle>:<Class>@<slot>. Two paths in:

  - load_extension(root): one manifest, one Bundle, registered at @official.
    Re-checks multi-bundle at runtime (defense-in-depth vs. the schema).
  - discover_inline_bundles(paths): each subfolder of LANGFLOW_COMPONENTS_PATH
    is a Bundle at @extra. Walk order is platform-independent (sorted dirs,
    user-declared path order). First-wins on duplicate names; second emits
    duplicate-inline-bundle warning that names both paths.

Manifest-first precedence helpers (installed_extension_roots,
manifest_owning_distributions, filter_plugin_entry_points) let callers of
the legacy langflow.plugins entry-point loader skip distributions that ship
a manifest, so component entry-points are not double-registered.

New typed error codes: module-import-failed, duplicate-component-name,
duplicate-distribution, duplicate-inline-bundle, inline-bundle-name-invalid.
Each ships with a format branch and a snapshot test.

Tests cover the AC: single-bundle happy path, multi-bundle rejection,
missing/empty/no-Component bundle, duplicate class names, deterministic
walk order, recursive discovery, inline-bundle first-wins + dot-dir skip
+ bundle.json metadata, manifest-first precedence partition, PEP-503
distribution-name canonicalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1015 review feedback

- Drop the unproduced duplicate-distribution error code; LE-1022 will add
  it once startup-time discovery has a place to surface it.  Restores the
  invariant that every code in ERROR_CODES has a producer.
- Clarify that intra-bundle relative imports are NOT supported in v0; only
  absolute references between bundle modules work in this milestone.
- Use strict=False when re-resolving bundle_root in the walker; the path
  was already existence-checked, and a concurrent removal in the narrow
  window should not raise across the loader's public boundary.
- Hoist the json import out of _read_inline_bundle_json's body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(lfx): split extension loader into a small subpackage (LE-1015)

Addresses the file-size feedback from the LE-1015 review.  The single
870-line loader.py becomes a flat package keyed off the four section
banners that already existed inline:

    loader/
      __init__.py     # re-exports the public surface
      _types.py       # SLOT constants, LoadedComponent, LoadResult
      _discovery.py   # filesystem walk + importlib.util orchestration
      _detection.py   # Component subclass identification (MRO heuristic)
      _orchestrator.py # load_extension, discover_inline_bundles
      _plugins.py     # manifest-first precedence over langflow.plugins

Largest file is now _orchestrator.py at 440 LOC (was 870); every file is
well under the 800-LOC project guideline.  No behavior change: the public
import paths from lfx.extension are unchanged, all 38 loader tests still
pass.  ``_canonicalize_distribution`` is now exported as
``canonicalize_distribution`` from ``loader._plugins`` (it's a stable
PEP-503 helper that downstream modules will reach for, so it loses the
private underscore).

The test suite is split to mirror the package:

    tests/unit/extension/loader/
      conftest.py             # shared fixtures, FakeDist, autouse scrub
      test_load_extension.py  # @official slot, identity, failure modes
      test_inline_bundles.py  # LANGFLOW_COMPONENTS_PATH, @extra slot
      test_plugins.py         # manifest-first precedence helpers
      test_types.py           # LoadedComponent, LoadResult, code parity

Each test file imports its own slice of the public API and pulls fixtures
from conftest, so a reader looking at one banner can read it in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: LangflowCompat -> LfxCompat

* fix: Remove references to ticket

* fix: encode maxItems constraint in schema

* fix: deferred fields and schema version

* Fix ruff errors

* fix: align loader tests with renamed manifest API and strip ticket refs

The merge of feat/extension-validate brought in the LfxCompat / compat
rename, but the loader test fixtures still used LangflowCompat / bundle_api
and failed at the manifest layer. Update conftest.py + test_load_extension.py
to the post-rename API.

Strip 17 LE-XXXX ticket references from loader source files, errors.py
loader-specific comments, and the four loader test docstrings, matching the
convention applied to LE-1014. Descriptive prose preserved.

Promote _BUNDLE_NAME_RE to BUNDLE_NAME_RE on the manifest module so the
loader's orchestrator no longer reaches across modules into a private name.

* feat(lfx): append-only migration table + flow deserializer rewrite hook

Adds the migration layer of the Extension System: an append-only JSON table
that maps three legacy component reference shapes (bare class name, old
import path, pre-Phase-A namespaced slot) to the post-Phase-A canonical
ext:<bundle>:<Class>@<slot> identifier, plus a deserializer hook that
rewrites a saved-flow payload in place against that table on load.

What landed:

  * lfx.extension.migration.schema -- Pydantic models for MigrationEntry +
    MigrationTable with per-entry validators (exactly one of bare/import/slot
    populated; canonical target shape) and table-level uniqueness check.
  * lfx.extension.migration.loader -- canonical in-repo path, threadsafe
    process-lifetime cache, typed errors on every failure mode.
  * lfx.extension.migration.rewrite -- node-by-node rewrite, idempotent on
    canonical refs, difflib-backed closest-match suggestion for unmapped
    references, cross-bucket ambiguity surfaces component-name-ambiguous
    instead of silently loading into the wrong bundle.
  * Wired into Graph.from_payload before validate_flow_for_current_settings
    so every saved-flow load goes through migration first.
  * scripts/migrate/check_migration_append_only.py -- CI guard that diffs
    the working-tree table against origin/main and rejects removals or
    target mutations; reordering and additions are allowed.
  * 34 new unit tests covering rewrite paths, loader failure modes, schema
    invariants, and the CI script behavior.

What is deliberately deferred:

  * flow-migrated event emission. The events pipeline is unavailable in this
    iteration; the wiring point in Graph.from_payload is marked TODO and the
    MigrationReport already carries every field a future emitter needs.

The shipped migration_table.json starts empty; entries land alongside the
pilot bundle extraction in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(frontend): palette Bundle reload action + loading + toasts (#13025)

* feat(lfx): add extension manifest schema, validate CLI, and error formatter (LE-1014)

Foundation for the Bundle Separation iteration. Defines what a valid
extension.json looks like (Pydantic models + Draft 2020-12 JSON Schema),
ships the offline `lfx extension validate` command, and ships the single
`format_extension_error` function that every other extension-system module
will use to render structured errors.

What's in lfx.extension:

- `ExtensionManifest` / `BundleRef` / `LangflowCompat` Pydantic models with
  `extra="forbid"` so unknown fields fail loudly. Deferred fields (`services`,
  `routes`, `hooks`, `starter_projects`, `userConfig`) are reserved as
  None-only so non-null values produce a dedicated
  `field-deferred-in-this-milestone` error instead of a generic schema wall.
  `bundles` accepts a list but rejects length > 1 with
  `multi-bundle-deferred-in-this-milestone` (validator-enforced; the loader
  re-checks at install time in LE-1015).
- `schema.build_schema()` + `build_schema_json()` produce the publishable
  artifact at schemas.langflow.org/extension/v1.json.
- `ExtensionError` typed envelope and `format_extension_error` -- one branch
  per discriminant. Codes are registered in `ERROR_CODES`; an
  `ExtensionError` constructed with an unknown code raises at construction
  time, preventing producers from shipping without a matching renderer.
- `validate_extension` runs four passes: manifest discovery + schema,
  path-safety (no `..`, no absolute paths, no symlink escape), AST
  inspection of every `.py` (syntax, Component subclass present, build()
  declared, top-level `import *`, top-level I/O primitives), and an opt-in
  `--execute-imports` that runs each module in a subprocess with a
  temporary HOME / TMPDIR / LANGFLOW_CONFIG_DIR and LANGFLOW_*/LFX_* env
  vars stripped.
- `lfx extension validate` and `lfx extension schema` typer subcommands.

Acceptance-criteria coverage in tests/unit/extension/:

- Round-trips every v0 manifest field; deferred fields rejected; multi-bundle
  rejected with the dedicated discriminant.
- JSON Schema validates the v0 example and rejects 12 malformed manifests
  with distinct error paths (>= 10 required by the ticket).
- Median default-validate runtime < 100ms on the basic template.
- Crafted side-effect bundle: default validate does NOT execute it (canary
  file is never written); `--execute-imports` DOES execute it and the canary
  appears, while LANGFLOW_* env vars are NOT inherited by the subprocess.
- Snapshot tests for every code in ERROR_CODES; a guard test verifies
  ERROR_CODES and the snapshot table are in lockstep so future additions
  cannot ship without a format branch and a snapshot.

Wiring: `lfx extension` is a sub-app under the Authoring help panel so
future tickets (LE-1016 init/dev, LE-1018 reload) can attach without
colliding with the existing `lfx validate` (which validates flow JSON, not
extensions).

* fix(lfx): fall back to tomli on Python 3.10 in extension manifest loader

tomllib is stdlib only on 3.11+, but lfx supports 3.10-3.13. Use the
existing tomli runtime dependency as the 3.10 fallback (same API, so the
import alias keeps the rest of the module unchanged).

Fixes ModuleNotFoundError seen in CI on the 3.10 job.

* Update src/lfx/tests/unit/extension/test_schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/schema.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/cli/_extension_commands.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* feat(lfx): add single-Bundle loader and LANGFLOW_COMPONENTS_PATH discovery (LE-1015)

Introduces lfx.extension.loader: the runtime that turns an Extension on disk
into LoadedComponent records keyed by ext:<bundle>:<Class>@<slot>. Two paths in:

  - load_extension(root): one manifest, one Bundle, registered at @official.
    Re-checks multi-bundle at runtime (defense-in-depth vs. the schema).
  - discover_inline_bundles(paths): each subfolder of LANGFLOW_COMPONENTS_PATH
    is a Bundle at @extra. Walk order is platform-independent (sorted dirs,
    user-declared path order). First-wins on duplicate names; second emits
    duplicate-inline-bundle warning that names both paths.

Manifest-first precedence helpers (installed_extension_roots,
manifest_owning_distributions, filter_plugin_entry_points) let callers of
the legacy langflow.plugins entry-point loader skip distributions that ship
a manifest, so component entry-points are not double-registered.

New typed error codes: module-import-failed, duplicate-component-name,
duplicate-distribution, duplicate-inline-bundle, inline-bundle-name-invalid.
Each ships with a format branch and a snapshot test.

Tests cover the AC: single-bundle happy path, multi-bundle rejection,
missing/empty/no-Component bundle, duplicate class names, deterministic
walk order, recursive discovery, inline-bundle first-wins + dot-dir skip
+ bundle.json metadata, manifest-first precedence partition, PEP-503
distribution-name canonicalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(lfx): address LE-1015 review feedback

- Drop the unproduced duplicate-distribution error code; LE-1022 will add
  it once startup-time discovery has a place to surface it.  Restores the
  invariant that every code in ERROR_CODES has a producer.
- Clarify that intra-bundle relative imports are NOT supported in v0; only
  absolute references between bundle modules work in this milestone.
- Use strict=False when re-resolving bundle_root in the walker; the path
  was already existence-checked, and a concurrent removal in the narrow
  window should not raise across the loader's public boundary.
- Hoist the json import out of _read_inline_bundle_json's body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(lfx): split extension loader into a small subpackage (LE-1015)

Addresses the file-size feedback from the LE-1015 review.  The single
870-line loader.py becomes a flat package keyed off the four section
banners that already existed inline:

    loader/
      __init__.py     # re-exports the public surface
      _types.py       # SLOT constants, LoadedComponent, LoadResult
      _discovery.py   # filesystem walk + importlib.util orchestration
      _detection.py   # Component subclass identification (MRO heuristic)
      _orchestrator.py # load_extension, discover_inline_bundles
      _plugins.py     # manifest-first precedence over langflow.plugins

Largest file is now _orchestrator.py at 440 LOC (was 870); every file is
well under the 800-LOC project guideline.  No behavior change: the public
import paths from lfx.extension are unchanged, all 38 loader tests still
pass.  ``_canonicalize_distribution`` is now exported as
``canonicalize_distribution`` from ``loader._plugins`` (it's a stable
PEP-503 helper that downstream modules will reach for, so it loses the
private underscore).

The test suite is split to mirror the package:

    tests/unit/extension/loader/
      conftest.py             # shared fixtures, FakeDist, autouse scrub
      test_load_extension.py  # @official slot, identity, failure modes
      test_inline_bundles.py  # LANGFLOW_COMPONENTS_PATH, @extra slot
      test_plugins.py         # manifest-first precedence helpers
      test_types.py           # LoadedComponent, LoadResult, code parity

Each test file imports its own slice of the public API and pulls fixtures
from conftest, so a reader looking at one banner can read it in isolation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(lfx): atomic-swap Bundle reload pipeline + endpoint + CLI (LE-1018)

Five-stage reload (parallel staging load -> validate -> swap under write
lock -> cleanup -> emit) for installed Bundles in Mode A.  In-flight
flows keep the pre-swap class via existing references; new flows pick up
the post-swap class atomically; concurrent reloads on the same Bundle
are rejected with reload-in-progress.

Adds:

* lfx/extension/registry.py -- BundleRegistry with per-bundle
  reload-in-progress guard and components_index.json writer
* lfx/extension/reload.py -- the five-stage pipeline; events emission
  is stubbed (TODO LE-1017) so the swap mechanics can ship before the
  events service lands
* loader: optional module_namespace param so Stage 1 lands in
  __reload_staging__.<id> instead of the live _lfx_ext.* namespace
* errors: four new typed reload codes (reload-in-progress,
  reload-bundle-not-installed, reload-bundle-name-mismatch,
  reload-source-missing) with branch templates and snapshot tests
* HTTP: POST /api/v1/extensions/{id}/bundles/{name}/reload, gated by
  the existing get_current_active_user dependency, returns 409 with a
  typed body for the in-progress collision case
* CLI: lfx extension reload <id> [--bundle <name>] -- HTTP client
  against the dev server with text/json output and proper exit codes
  (--all is gated until LE-1019 lands the list endpoint)
* tests: 16 reload-pipeline tests covering the AC matrix (rename
  round-trip, broken-bundle isolation, concurrent readers, in-flight
  flow, double-reload guard, bundle-name mismatch) plus 9 CLI client
  tests

Mode A only.  In Mode B/C bundle changes require a Docker image rebuild
and the reload path is not exercised.

The events emission in Stage 5 is intentionally stubbed -- the LE-1017
ticket will swap the body of _emit_bundle_reload_event in one place
without touching the pipeline core.

* feat(frontend): palette Bundle reload action + loading + toasts

Adds the frontend half of the bundle reload flow.  When the Bundle
header is right-clicked or its overflow ("⋮") icon clicked, a Reload
action fires POST /api/v1/extensions/{id}/bundles/{name}/reload and
surfaces the result via the existing alert-store toast system.

What landed (frontend only):

  * src/controllers/API/queries/extensions/ -- typed wire-format models
    (ReloadBundleResponse, ExtensionErrorPayload, ReloadInProgressDetail)
    and the useReloadBundle mutation hook.  The hook unwraps the 409
    `reload-in-progress` detail into a stable, parseable Error message so
    the UI can branch without reading status codes.
  * components/bundleHeaderActions.tsx -- new Select-based overflow menu
    next to the Bundle header chevron.  Three toast paths: success
    (green, with components +/- delta), structural failure (red, with
    typed errors and inline hints), reload-in-progress (notice).
    Loading state swaps the kebab icon for a spinning Loader2 while
    the request is in flight.  Renders nothing when no extension_id is
    on the bundle, so the static SIDEBAR_BUNDLES list is unaffected.
  * components/bundleItems.tsx -- wires the new actions in next to the
    chevron, plus a context-menu (right-click) capture that opens the
    same overflow trigger so keyboard / mouse / right-click all share
    one source of truth.
  * types/index.ts -- BundleItemProps.item gains an optional
    extension_id; took the opportunity to extract the SidebarBundle
    interface and tighten three pre-existing `any` types.
  * customization/feature-flags.ts -- ENABLE_EXTENSION_RELOAD gate, off
    by default until the bundle-list endpoint that populates extension_id
    per bundle ships.
  * controllers/API/helpers/constants.ts -- EXTENSIONS URL constant.
  * Tests: 7 component tests + 3 mutation-hook tests, all green.  Total
    sidebar + extensions test count: 479 passing.

Deferred:

  * Event-pipeline subscription is left as an inline TODO.  The mutation
    response carries enough information to drive the toasts on its own
    today; once the events service lands the toast wiring will move to
    a `bundle_reloaded` / `bundle_reload_failed` listener so multi-tab
    and multi-worker swaps surface exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: End to end bundle installation

* fix: Review comments addressed

* Update component_index.json

* Update component_index.json

* Update router.py

* fix(docker): copy src/bundles before uv sync so workspace bundles resolve

Each directory under ``src/bundles`` is a uv workspace member referenced by
``langflow-base`` (and the root project) as a path dependency.  The Docker
builders ran ``uv sync --no-install-project`` after copying only the
top-level pyproject.toml files, so resolution failed with
``Distribution not found at: file:///app/src/bundles/<name>``.

Copying the whole ``src/bundles`` tree (rather than enumerating each
bundle) means a new bundle dropped under that dir does not require a
Dockerfile edit.  The full ``./src`` copy a few lines later produces the
same final layer either way; this earlier copy just unblocks the
dependency-resolution sync.

Touched all builders that run a workspace-resolving uv sync:

  * docker/build_and_push.Dockerfile
  * docker/build_and_push_base.Dockerfile
  * docker/build_and_push_ep.Dockerfile
  * docker/build_and_push_with_extras.Dockerfile
  * docker/dev.Dockerfile (bind-mount instead of COPY)

* Revert "fix(docker): copy src/bundles before uv sync so workspace bundles resolve"

This reverts commit 5aa008a3cd.

* feat: DuckDuckGo as Extension in new Bundle System (#13044)

* feat: DuckDuckGo Extension for bundles

* fix ruff errors

* Update test_pilot_duckduckgo_upgrade.py

* test(lfx): handle non-empty canonical migration table + editable installs

- test_path_override_bypasses_cache: mirror the cached canonical table
  into the temp file instead of asserting it's empty.  Drift in
  migration_table.json (the duckduckgo entries shipped with the B1
  pilot) no longer breaks the cache-bypass invariant.

- test_lfx_duckduckgo_ships_manifest: editable installs (pip install -e)
  surface only dist-info entries in dist.files, so we cannot use that
  path to find extension.json in the workspace venv.  Detect editable
  mode via direct_url.json's PEP 660 marker and fall through to walking
  the source tree; non-editable wheel installs still exercise the
  dist.files path the loader uses at runtime.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix(docker): copy src/bundles before uv sync so workspace bundles resolve

Each directory under ``src/bundles`` is a uv workspace member referenced by
``langflow-base`` (and the root project) as a path dependency.  The Docker
builders ran ``uv sync --no-install-project`` after copying only the
top-level pyproject.toml files, so resolution failed with
``Distribution not found at: file:///app/src/bundles/<name>``.

Copying the whole ``src/bundles`` tree (rather than enumerating each
bundle) means a new bundle dropped under that dir does not require a
Dockerfile edit.  The full ``./src`` copy a few lines later produces the
same final layer either way; this earlier copy just unblocks the
dependency-resolution sync.

Touched all builders that run a workspace-resolving uv sync:

  * docker/build_and_push.Dockerfile
  * docker/build_and_push_base.Dockerfile
  * docker/build_and_push_ep.Dockerfile
  * docker/build_and_push_with_extras.Dockerfile
  * docker/dev.Dockerfile (bind-mount instead of COPY)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: Review sweep

* fix: More review comments addressed

* Ruff check

* docs: add bundle porting guide

Step-by-step recipe for extracting a provider package from the in-tree
``src/lfx/src/lfx/components/<provider>/`` directory into a standalone
Extension Bundle distribution under ``src/bundles/<provider>/``.

The DuckDuckGo bundle is the reference; every section maps to a single
copy-pasteable change and a verification command.

Doc surfaces a forthcoming ``scripts/migrate/port_bundle.py`` automation
helper for the mechanical bits; that script lands on the next branch
together with the second-pilot port that validates the recipe end-to-end.

* fix(extension): accept Output(method=...) in build-method validator

The validator's static AST check looked for a literal ``build`` method
on every Component subclass and emitted ``build-method-missing`` when
absent.  Real Langflow components -- including the production
``DuckDuckGoSearchComponent`` and the in-tree ``ArXivComponent`` -- do
not declare a literal ``build``; they declare entry-points
declaratively via ``outputs = [Output(name=..., method=<name>)]`` and
the named method is what gets called.  Result: every clean port hit a
spurious ``build-method-missing`` error from ``lfx extension validate``.

Fix: extend ``_has_build_method`` to also accept any class that names a
method via ``Output(method="X")`` AND defines that method in the class
body.  The negative case ("Output(method=...) without a matching def")
still fires -- a typo'd method name would crash at runtime, so the
static check should keep flagging it.

Two new tests in ``test_validate.py`` lock the contract: positive case
mirroring duckduckgo / arxiv, negative case for typo'd method names.
All 26 validator tests pass.

PORTING.md updates fall out of running the recipe live against the
duckduckgo bundle on this branch:

  * Validate path is ``src/bundles/<bundle>/src/lfx_<bundle>``, not the
    bundle root (the manifest lives next to ``__init__.py``).
  * Index regen needs ``LFX_DEV=1`` to skip the prebuilt-index fast path
    and uses ``scripts/build_component_index.py``.
  * Drop the migration-table JSON from the ruff invocation (ruff treats
    it as Python and complains about the top-level expression).

* fix: Delete outdated components

* fix(palette): wire reload kebab via DropdownMenu, not Select

The reload kebab on the palette Bundle header was using Radix ``Select``
to back its overflow menu.  ``Select`` is for picking a value, not for
firing an action: ``onValueChange`` is gated by value-equality (so a
re-click of the only item is a no-op), and the popover-portal click
semantics interact poorly with the parent disclosure-trigger button.
The combined effect: clicking ⋮ → Reload opened the popover and closed
it, but never fired the network request, with no console error to point
at.

Switch to ``DropdownMenu`` (purpose-built action menu).
``DropdownMenuItem`` exposes ``onSelect`` which fires on every
activation -- the same callback path keyboard navigation uses -- so
clicking Reload reliably invokes the mutation.

Test mocks updated to drive the DropdownMenu primitives instead of
Select; all 8 bundleHeaderActions tests + the broader 437-test sidebar
suite still pass.

* fix(extension): route ddgs through the lockfile + drop bogus list endpoint hint

Two reviewer findings, both about reproducibility / correctness of
operator-facing surfaces.

[P2] ``docker/build_and_push_base.Dockerfile`` previously installed
``ddgs`` via an unpinned ``uv pip install ddgs`` after the workspace
sync.  That made the base image non-reproducible: a future ``ddgs``
release would silently drift from the tested lock state on every
rebuild.

Fix: route ddgs through the locked sync.

  * Restore the ``duckduckgo`` extra in ``src/backend/base/pyproject.toml``
    (``ddgs>=9.0.0``) as an internal "image-build sidecar".  Public
    consumers still install ``lfx-duckduckgo`` directly; the extra
    exists only to keep ``ddgs`` resolved in the lockfile.
  * The Dockerfile now passes ``--extra duckduckgo`` to ``uv sync
    --frozen``, picking up the locked ``ddgs==9.14.1``.
  * The follow-on bundle install keeps ``--no-deps`` so it does not
    duplicate the now-locked ``ddgs`` / ``lfx`` / ``langchain-community``.

[P3] ``langflow/api/v1/extensions.py`` returned a fix hint that pointed
operators at ``GET /api/v1/extensions`` -- a route that does not exist
in this PR (it lands with the LE-1019 list endpoint).  Replaced with a
reference to ``lfx extension list``, which is shipped here.

* fix(compat): bridge langflow.components.* dynamically via meta path finder

Commit 45552cd1df ("fix: Delete outdated components") removed the
physical shim files under ``src/backend/base/langflow/components/``
that forwarded saved-flow imports like
``from langflow.components.processing.converter import convert_to_dataframe``
or ``import langflow.components.knowledge_bases.retrieval`` to their
new ``lfx.components.*`` homes.  Without those shims, dotted imports
into ``langflow.components.<sub>.<leaf>`` failed at flow-load time --
the existing ``LangflowCompatibilityModule`` registers ``langflow.components``
itself in ``sys.modules`` but does not bridge submodules, so Python's
import machinery falls through to the now-empty langflow.components
directory and raises ``ModuleNotFoundError``.

Replace the deleted physical-shim stack with a single ``MetaPathFinder``
in ``langflow/__init__.py`` that dynamically resolves every
``langflow.components.<rest>`` import to ``lfx.components.<rest>`` and
registers the loaded lfx module in ``sys.modules`` under both names.
The langflow- and lfx-prefixed imports share a single underlying module
object, so class identity is preserved across the bridge -- ``isinstance``
checks against types resolved through either path keep working.

The finder also carries a small first-segment override map for the few
subpackages whose name diverged during the move; the only entry today is
``knowledge_bases`` -> ``files_and_knowledge``, matching the deleted
shim's intent.

Why a meta finder instead of restoring the physical shim files (option
1 from the scope analysis): the meta finder scales without per-bundle
maintenance.  Every future bundle extraction landed under ``lfx.components``
becomes reachable via the legacy ``langflow.components.<bundle>`` path
immediately; nobody has to remember to add a parallel langflow shim.

Six new unit tests in ``test_langflow_components_compat_shim.py`` lock
the contract: dotted submodule resolution, helpers re-export, the
``knowledge_bases`` override, class identity preservation, top-level
aliasing, and the "arbitrary extracted bundle" case that prevents
regression for future ports.

Verified pre-existing tests:
  * 17 dynamic-import integration tests pass.
  * The reload-route-guard suite still passes.
  * The Research Translation Loop starter-project test (which loads
    ArXivComponent) passes.

* Update index.tsx

* fix: Review pass on delivery

* fix: Second review sweep, bare names

* fix: Third review sweep

* Resolve dotted imports and attribute chains

Record import shape and flatten attribute chains so router references can be resolved across dotted imports and re-exports. Added ImportTarget dataclass, _attribute_chain helper, and switched IncludeCall/DecoratorRef to store attribute tuples. parse_file now records ImportTarget entries for imports and captures dotted parent/child chains for include_router and decorators. Replaced _resolve_var with _resolve_chain which handles "from" vs "module" imports, various import shapes (including import x.y and aliased imports), and prevents infinite recursion on re-export cycles.

* One more sweep

* Add seed-directory extension loading and docs

Introduce filesystem "seed directory" extension support and authoring docs. Adds three documentation pages (quickstart, manifest reference, author guide) and wires them into the docs sidebar. Implement load_seed_extensions to discover/load bundles from $LANGFLOW_SEED_DIR (default /opt/langflow/bundles), export it from loader/__init__ and extension package, and integrate it into the components import pipeline. Handle installed-vs-seed shadowing by preferring installed distributions and appending a typed seed-bundle-shadowed ExtensionError; add the corresponding error code and message. Update schema doc link and add unit/integration tests to cover seed loading, determinism, shadowing behavior, and migration-target resolution.

* Fix claims

* Fix docasaurus build

* Clean up the manual checklist

* Update test_pilot_duckduckgo_upgrade.py

* fix: Some wording issues with dogfooding

* fix: Comments addressed

* Update port_bundle.py

* Update component_index.json

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix: pytest testpaths glob and accurate scaffold output in port_bundle

testpaths now uses ``src/bundles/*/tests`` so future bundle tests are
picked up automatically without hardcoding each bundle at the root.
pytest expands the glob via ``glob.iglob(..., recursive=True)``.

Rewrote port_bundle.py's ``_render_migration_entries`` and
``_render_test_scaffold`` to match the actual conventions:

* Migration entries now use ``bare_class_name``/``import_path``/
  ``legacy_slot`` + ``target`` + ``added_in`` (the keys
  ``lfx.extension.migration.loader`` actually reads) instead of the
  invented ``from``/``to``/``release`` shape.
* Test scaffold now imports ``load_migration_table`` and
  ``migrate_flow_payload`` -- the APIs the real tests use -- instead of
  a fictitious ``resolve_legacy_id``.  Includes the ``migration_table``
  fixture, ``_saved_flow``/``_saved_flow_node`` helpers, and the
  distribution-importable + manifest-shipped checks that mirror
  ``test_pilot_duckduckgo_upgrade.py``.

Verified by rendering the scaffold for duckduckgo with synthetic plan
data, dropping it into ``src/lfx/tests/integration/extension/``, and
running pytest -- ``3 passed, 2 skipped`` (skips are the wheel-only
checks, by design when run inside the lfx isolation venv).

* [autofix.ci] apply automated fixes

* fix: Review comments

* Update test_discovery.py

* feat: Port Arxiv to the Extension Framework (#13047)

* feat(bundles): port arxiv as the second-pilot Bundle + porting helper

Validates ``src/bundles/PORTING.md`` end-to-end by following its recipe
against a clean candidate (``ArXivComponent``: no third-party runtime
deps, no langflow-base extra, no deactivated duplicate).  Touchpoints
exercised:

  * Bundle skeleton at ``src/bundles/arxiv/`` mirroring duckduckgo.
  * In-tree provider directory removed from ``src/lfx/src/lfx/components/``
    along with its three references in ``components/__init__.py``.
  * Workspace wiring: dep, ``[tool.uv.sources]``, ``[tool.uv.workspace]``
    members, lockfile.
  * Migration table: bare-name + two import-path forms + legacy_slot
    entry.
  * Component index regenerated via ``LFX_DEV=1`` (forces dynamic
    discovery; without it the script reproduces stale entries).
  * Integration test ``test_pilot_arxiv_upgrade.py`` mirroring the
    duckduckgo pilot suite; 5 tests pass against the workspace install.

PORTING.md updates fall out of running the recipe live:
  * Validate path is ``src/bundles/<bundle>/src/lfx_<bundle>``, not the
    bundle root (the manifest lives next to ``__init__.py``).
  * Index regen needs ``LFX_DEV=1`` to skip the prebuilt-index fast path.
  * Drop the migration-table JSON from the ruff invocation (ruff treats
    it as Python and complains about the top-level expression).

``scripts/migrate/port_bundle.py`` is the mechanical helper referenced
from § Automation: stdlib-only, dry-run by default, refuses on invalid
input, and intentionally leaves migration-table edits + integration-test
authoring to a human (release version + bare-name uniqueness require
judgement).  Three guard rails verified: invalid bundle name,
existing-target-bundle, missing in-tree provider.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* Update Research Translation Loop.json

* Update .secrets.baseline

* fix: Move bundle test for arxiv

* Update PORTING.md

* Update component_index.json

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* Update component_index.json

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* Update component_index.json

* Update component_index.json

* Update component_index.json

* fix: empty cache dict on reload

* Lint fixes

* Update test_components_cache_integration.py

* Update test_reload.py

* Hardening pass

* Update test_init_template.py

* fix: resolve cross-source bundle-name shadowing before registry population

The reload pipeline reads ``live.source_path`` from the registry on every
call.  Previously, the registry-population loop in
``import_extension_components`` only special-cased installed-shadows-seed;
for every other pair (seed/dev, seed/inline, dev/inline,
installed/dev, installed/inline) it silently overwrote earlier records
via last-wins iteration order.  A stale dev registration whose
``source_path`` pointed at a different filesystem location could clobber
the seed record's ``source_path``; reload would then walk the dev path
while the operator edited the seed copy on disk -- producing 200 OK with
empty deltas on every reload, including for syntactically broken edits.

Generalize the dedup pass to every (earlier, later) pair using the
explicit precedence ``installed > seed > dev > inline``, applied before
both registry population AND palette template construction so the two
read from the same winning source.  Add a new generic ``bundle-shadowed``
typed error code for the pairs the existing ``seed-bundle-shadowed`` did
not cover; keep ``seed-bundle-shadowed`` for the documented installed-
over-seed pair so existing CLI exit-code logic and snapshot tests stay
intact.  Both codes are warn-only in ``lfx extension list``.

Also add an INFO log line in reload Stage 1 with the resolved
``source_path`` so the next "200 OK with empty deltas" repro can be
triaged from the server log alone -- if the path logged is not the path
the operator was editing, this dedup is what to look at.

Includes a regression test
(``test_seed_bundle_shadows_dev_emits_generic_bundle_shadowed``) that
asserts both halves: seed wins in the BundleRegistry AND the registry's
``source_path`` is the seed path, not the stale dev path.

* Update component_index.json

* fix: widen lfx-* bundles' requires-python to <3.15 to match langflow root

The two pilot bundles (``lfx-arxiv``, ``lfx-duckduckgo``) were scaffolded
by ``scripts/migrate/port_bundle.py`` whose template hard-coded
``requires-python = ">=3.10,<3.14"``.  Because both bundles are uv
workspace members of the langflow root, that cap leaked into every
workspace-level resolve: ``cd src/lfx && uv sync`` (the path
``make lfx_tests`` takes) refuses to pick a Python 3.14 interpreter even
though lfx itself, langflow, and langflow-base all advertise
``>=3.10,<3.15``.  Hosts with 3.14 installed see:

    error: The requested interpreter resolved to Python 3.14.5, which
    is incompatible with the project's Python requirement: `>=3.10, <3.14`.

Fix at the source so future ``port_bundle.py`` runs do not regress this:
template emits ``<3.15``, and the two existing emitted pyprojects are
bumped in lockstep.  ``uv.lock`` regenerates with the wider range.

Verified ``make lfx_tests`` resolves cleanly on a host with both 3.13.12
and 3.14.5 installed; the focused extension slice runs to completion.

* Update __init__.py

* Update src/lfx/tests/unit/extension/migration/test_rewrite.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/loader/_plugins.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/docs/Deployment/deployment-extensions-production.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/lfx/src/lfx/extension/loader/_orchestrator.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/docs/Develop/extensions-author-guide.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update docs/docs/Develop/extensions-manifest.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: Coderabbit review suggestions

* Template updates

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Tweaks to bundle hot reload

* [autofix.ci] apply automated fixes

* chore: Address review comments by @Cristhianzl

* [autofix.ci] apply automated fixes

* Update BUNDLE_API.md

* Update component_index.json

* Update component_index.json

* Update Structured Data Analysis Agent.json

* fix: Next round of review comments

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-18 17:36:56 +00:00