Files
langflow/BUNDLE_API.md
Eric Hare 7139d07033 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
2026-06-11 11:30:53 -07:00

467 lines
24 KiB
Markdown

# Bundle API
Stable surface that Langflow Extension Bundles consume. Every public symbol
listed below is part of the contract: changes to its name, signature, semantics,
or visibility require a coordinated version bump and a `## Changelog` entry.
This document is paired with the integer **`BUNDLE_API_VERSION`** declared in
[`lfx.extension.manifest`](src/lfx/src/lfx/extension/manifest.py). Manifests
declare the contract versions they support via `lfx.compat: ["1"]`; a bundle
that does not list `str(BUNDLE_API_VERSION)` is rejected at install time with
`version-constraint-unsatisfied`.
> **CI gate:** any PR that modifies a file containing an in-scope surface MUST
> add a `## Changelog` entry describing the change. The CI guard
> [`scripts/migrate/check_bundle_api_changelog.py`](scripts/migrate/check_bundle_api_changelog.py)
> enforces this. Pure-internal refactors that preserve every public symbol's
> name and signature do not require a changelog entry, but reviewers should be
> skeptical.
---
## Surface (v0)
### Component base class
| Symbol | Source |
| --- | --- |
| `Component` | `lfx.custom.custom_component.component.Component` |
| `Component.build()` (declared on subclasses) | call site of every loaded bundle module |
| `Component.inputs` | declarative input list |
| `Component.outputs` | declarative output list |
| `Component.display_name` / `Component.description` / `Component.icon` / `Component.documentation` | metadata read by the palette |
| `Component.name` | optional override of the registry class name |
### Inputs
| Symbol | Source |
| --- | --- |
| `Input` (base) | `lfx.io` |
| `MessageTextInput` / `MultilineInput` / `SecretStrInput` | `lfx.io` |
| `IntInput` / `FloatInput` / `BoolInput` | `lfx.io` |
| `DropdownInput` / `TabInput` | `lfx.io` |
| `DictInput` / `NestedDictInput` | `lfx.io` |
| `FileInput` / `LinkInput` | `lfx.io` |
| `HandleInput` | `lfx.io` |
### Outputs
| Symbol | Source |
| --- | --- |
| `Output` | `lfx.io` |
### Schema types
| Symbol | Source |
| --- | --- |
| `Data` | `lfx.schema.data` |
| `DataFrame` | `lfx.schema.dataframe` |
| `Message` | `lfx.schema.message` |
### Lazy-import helper (consumed by bundle ``__init__.py`` files)
| Symbol | Source |
| --- | --- |
| `import_mod(attr_name, module_name, package)` | `lfx.utils.lazy_import` (canonical; `lfx.components._importing` re-exports it for backward compatibility) |
### Manifest contract (consumed by the loader)
| Symbol | Source |
| --- | --- |
| Manifest schema (`extension.json` / `[tool.langflow.extension]`) | `lfx.extension.manifest.ExtensionManifest` |
| `BundleRef` (one entry in `bundles[]`) | `lfx.extension.manifest.BundleRef` |
| `LfxCompat` (declared as `manifest.lfx`) | `lfx.extension.manifest.LfxCompat` |
| `BUNDLE_API_VERSION` (the integer this lfx ships) | `lfx.extension.manifest` |
| `EXTENSION_SCHEMA_URL` / `SCHEMA_VERSION` | `lfx.extension.manifest` |
Slot vocabulary: `official` (installed pip distributions and seed
directories) and `extra` (paths declared in `LANGFLOW_COMPONENTS_PATH`).
Component IDs at runtime are `ext:<bundle>:<Class>@<slot>`.
### Discovery + loading entry points
| Symbol | Source |
| --- | --- |
| `load_extension(root)` | `lfx.extension.loader` |
| `load_installed_extensions()` | `lfx.extension.loader` |
| `discover_inline_bundles()` | `lfx.extension.loader` |
| `discover_installed_extensions()` / `discover_seed_extensions()` / `discover_all_extensions()` | `lfx.extension.discovery` |
| `LoadedComponent` | `lfx.extension.loader` (frozen dataclass; what the registry stores) |
| `LoadResult` | `lfx.extension.loader` |
| `SLOT_OFFICIAL` / `SLOT_EXTRA` | `lfx.extension.loader` |
### Reload pipeline
| Symbol | Source |
| --- | --- |
| `reload_bundle(registry, bundle_name)` | `lfx.extension.reload` |
| `BundleRegistry` | `lfx.extension.bundle_registry` |
| `BundleRecord` | `lfx.extension.bundle_registry` |
| `ReloadInProgressError` | `lfx.extension.bundle_registry` |
| `POST /api/v1/extensions/{id}/bundles/{name}/reload` | `langflow.api.v1.extensions` |
### Errors
| Symbol | Source |
| --- | --- |
| `ExtensionError` | `lfx.extension.errors` |
| `ExtensionErrorCollection` | `lfx.extension.errors` |
| `format_extension_error(error)` | `lfx.extension.errors` |
| `ERROR_CODES` (frozenset of every typed code) | `lfx.extension.errors` |
The full kebab-case discriminant set is the contract — adding a code is
backward-compatible; removing or renaming a code is a breaking change and
requires a `BUNDLE_API_VERSION` bump.
### Validate / authoring CLI
| Symbol | Source |
| --- | --- |
| `validate_extension(root, *, execute_imports=False)` | `lfx.extension.validate` |
| `ValidateReport` | `lfx.extension.validate` |
| `lfx extension validate` (CLI) | `lfx.cli._extension_commands` |
| `lfx extension schema` (CLI) | `lfx.cli._extension_commands` |
| `lfx extension init` (CLI) | `lfx.cli._extension_commands` |
| `lfx extension dev` (CLI -- registers a local path and execs `langflow run`) | `lfx.cli._extension_commands` |
| `lfx extension list` (CLI) | `lfx.cli._extension_commands` |
| `lfx extension reload` (CLI) | `lfx.cli._extension_commands` |
| `register_dev_extension` / `unregister_dev_extension` (Python API) | `lfx.extension.dev_registry` |
### Migration
| Symbol | Source |
| --- | --- |
| Migration-table file | `src/lfx/src/lfx/extension/migration/migration_table.json` |
| `MigrationEntry` | `lfx.extension.migration.schema` |
| `MigrationTable` | `lfx.extension.migration.schema` |
| `migrate_flow_payload(payload, table)` | `lfx.extension.migration.rewrite` |
| `MIGRATION_SCHEMA_VERSION` | `lfx.extension.migration.schema` |
---
## Out of scope (v0)
These are reserved in the manifest schema and produce a typed
`field-deferred-in-this-milestone` error if set; they are NOT part of the
v0 contract:
- `services` — bundle-declared service factories
- `routes` — bundle-mounted HTTP routes
- `hooks` — bundle-declared lifecycle hooks
- `starter_projects` — bundle-shipped starter flows
- `userConfig` — bundle-declared user-config schema
- Multi-bundle manifests (`bundles` list with length > 1)
---
## Pilot bundle: `lfx-duckduckgo`
The shipped LE-1023 pilot is **`duckduckgo`**, extracted into the
standalone distribution
[`lfx-duckduckgo`](src/bundles/duckduckgo/) under `src/bundles/duckduckgo/`
with its own `pyproject.toml`. `langflow`'s own `pyproject.toml`
declares `lfx-duckduckgo>=0.1.0` as a regular dependency so a flat
`pip install langflow` continues to ship the bundle as before.
Why this bundle:
- Single component (`DuckDuckGoSearchComponent`) in a single file
(`duck_duck_go_search_run.py`).
- Zero git churn over the last six months.
- Modern `Component` base class (no `LCToolComponent` legacy).
- No authentication required — failure mode is a single failed request, not a
paid-API outage.
- Class name is globally unique across `src/lfx/src/lfx/components/**`, so the
bare-name migration entry is allowed by `check_bare_names.py`.
The runtime half of the M1 proof-of-delivery gate (save a flow on
pre-migration Langflow, upgrade, confirm it loads AND runs identically)
lives in the dogfood checklist at
[`src/bundles/duckduckgo/M1_DOGFOOD_CHECKLIST.md`](src/bundles/duckduckgo/M1_DOGFOOD_CHECKLIST.md);
the deserialize half is covered by
`src/lfx/tests/integration/extension/test_pilot_duckduckgo_upgrade.py`.
---
## Changelog
### v0 (this release)
- Initial surface enumerated above. Frozen as `BUNDLE_API_VERSION = 1`.
- `BundleRegistry.write_locked()` exposed as a public context manager so the
reload pipeline can hold the registry write lock across both the
`sys.modules` swap and the `BundleRecord` install. Concurrent readers
can no longer observe new modules paired with the old record. No change
to the addressable component contract.
- HTTP reload endpoint (`POST /api/v1/extensions/{id}/bundles/{name}/reload`)
returns `422 Unprocessable Entity` for structural failures (broken
bundle, missing source path, name mismatch) instead of `200 OK` with
`ok=false`. Body is `{...primaryError, result: ReloadResult}` so the
full typed result is preserved under the FastAPI `detail` envelope.
`409 Conflict` for `reload-in-progress` is unchanged.
- CLI table updated to remove the obsolete `dev register` / `dev unregister`
/ `dev list` subcommands; the actual surface is `extension dev <path>`
plus the Python helpers `register_dev_extension` / `unregister_dev_extension`.
- `MigrationTable.ambiguous_bare_names` added. Each entry is
`{name, candidates: [list of canonical IDs]}` and registers a bare
class name that exists in 2+ bundles. The deserializer now surfaces
`component-name-ambiguous` (with the candidate targets) for any bare
name listed here, instead of falling through to the generic
`component-not-found-with-hint`. Seeded with the canonical regression
cases (`MergeDataComponent`, `SplitTextComponent`, `SubFlowComponent`).
`check_bare_names.py` now verifies every Component class found in
2+ bundle folders has a matching marker, so a future bundle move that
introduces a new ambiguity is caught at PR time.
- Router-trust CI guard broadened to scan every `.py` under
`src/backend/base/langflow/api/**` and `src/lfx/src/lfx/**`; a new file
that mounts an `APIRouter(prefix=".../extensions...")` is auto-detected
and checked for forbidden install/uninstall/registry-mutation handlers.
Authors of files with non-literal prefixes can opt in via a
`# router-trust: in-scope` marker.
- Router-trust guard rewritten to use AST-based cross-file resolution.
A forbidden handler in module A is now caught when module B mounts A's
router via `parent.include_router(child, prefix=".../extensions...")`,
and the same applies transitively across multi-hop include_router
chains. An imported router that cannot be statically resolved is
ignored (the guard never flags routes it cannot prove reachable from
`/extensions`); routes co-located with an in-scope router ARE flagged.
- `check_migration_append_only.py` now compares
`ambiguous_bare_names` alongside `entries`. A marker may not be
removed once published, and its `candidates` list may only grow --
shrinking it would silently regress flows from
`component-name-ambiguous` to `component-not-found-with-hint`.
- Router-trust guard now resolves dotted attribute references in
`include_router` and decorators. ``include_router(child.api.router,
prefix="/extensions")`` after ``import child.api`` (and the
``import child.api as alias; alias.router`` shape) are caught -- not
just ``from child.api import router as child_router``. The parser
flattens any ``Name``/``Attribute`` chain, and the resolver walks
imports of either kind (``from M import N`` and ``import M``,
with or without an asname) back to the source file.
- Router-trust guard's relative-import resolver is now
``__init__.py``-aware. Inside a package, ``from .child import Y``
anchors at the package itself (level=1 -> ``pkg``); inside a regular
module ``pkg.foo`` it anchors at the parent package (level=1 ->
``pkg``). The arithmetic differs because ``__init__.py``'s file
module IS the package, while ``pkg/foo.py``'s file module is
``pkg.foo``. The resolver tracks ``is_package`` and decrements
``level`` by one for ``__init__.py`` files so both shapes resolve
correctly.
- Code-review hardening pass across the extension subsystem. No public
symbol's name or signature changed; this entry covers behavioural
tightening that bundle authors and operators should be aware of:
- **Path-safety contract honored on every discovery path.**
``DiscoveredExtension`` records emitted from
``discover_installed_extensions`` / ``discover_seed_extensions``
now run the same resolve-and-``relative_to`` containment check that
``validate_extension`` performs. A symlinked ``bundles[0].path``
or a symlinked seed subdirectory that escapes the extension root
is now rejected with ``path-escape`` *before* reaching the loader,
instead of slipping through to ``exec_module()``. The shared
primitive lives at ``lfx.extension._paths.is_within``; every
walker (loader, validator, seed discovery, inline-bundle discovery)
uses the same function and the same ``SKIP_DIR_NAMES``.
- **``--execute-imports`` env allowlist.** The validator's
``--execute-imports`` subprocess now inherits an explicit allowlist
(``PATH``, ``LANG``, ``LC_*``, ``SYSTEMROOT``, ``TMPDIR``, ``TZ``,
Python locale + encoding vars) instead of denylisting only
``LANGFLOW_*``/``LFX_*``. Cloud / CI credentials
(``AWS_*``, ``OPENAI_API_KEY``, ``GITHUB_TOKEN``, ...) no longer
propagate into untrusted bundle import. The CLI / module docs
re-frame this pass as best-effort hygiene lint, not a sandbox.
- **AST hygiene lint widened.** ``_find_top_level_io`` now flags
``exec``, ``eval``, ``__import__``, ``compile`` as top-level
primitives and ``importlib.import_module`` /
``importlib.__import__`` as dotted-name primitives. Still
best-effort literal-name matching; trivially bypassable by
obfuscation, and documented as such.
- **Reload swap is non-destructive.** ``_swap_sys_modules`` now
builds the staging->prod rename map **before** any ``sys.modules``
mutation, snapshots popped old modules into a recovery map, and
restores them on any mid-swap exception. The length-mismatch
tripwire on ``zip(strict=True)`` no longer leaves the prod
namespace shredded. A new typed code,
``reload-class-retag-failed``, is appended to
``ReloadResult.warnings`` when ``cls.__module__`` cannot be
retagged so the empty-palette-after-reload regression leaves a
trail instead of silently failing.
- **Cross-source bundle-name collision.**
``load_installed_extensions`` now detects two distributions with
different canonical names but identical ``bundle.name`` (which
would silently clobber each other at
``_lfx_ext.official.<name>.*``) and emits a typed
``duplicate-bundle-name`` error on the loser, dropping its
components. ``BundleRegistry.install_bundle`` additionally logs a
WARNING when an existing record is replaced by a record from a
different ``source_path`` (catches collisions the upstream
precedence resolver missed).
- **Reload endpoint off event loop.**
``POST /api/v1/extensions/{id}/bundles/{name}/reload`` now invokes
``reload_bundle`` via ``asyncio.to_thread`` so slow or large
bundle imports do not freeze the worker for other in-flight
requests. The wire contract (status codes, body shape) is
unchanged.
- **Stable typed-error code rename.**
``multi-bundle-deferred-in-this-milestone`` is renamed to the
stable ``multi-bundle-unsupported``. The old code is retained in
``ERROR_CODES`` as a deprecated alias for one milestone for log
scrapers. Three new codes are added to
``ERROR_CODES``: ``duplicate-bundle-name`` (see above),
``reload-class-retag-failed`` (see above), and
``reload-transport-error`` (CLI-side connectivity failure,
previously misreported as ``reload-source-missing``).
- **Discovery preserves "unreadable" vs "absent" distinction.**
``_pyproject_declares_extension`` now propagates ``OSError`` so a
permission failure on a pyproject that *might* declare an
extension surfaces as ``manifest-unreadable`` instead of being
silently dropped as "no extension here".
- **Dev registry corruption is logged.** ``_read_state`` now
distinguishes file absent (silent, legitimate empty registry),
file present but unreadable (WARNING), and file present but
corrupt JSON / wrong shape (WARNING with detail). The state
file is written with mode 0600 so a hostile third-party process
cannot inject an extension path into the developer's next
``langflow run``.
- **Entry-point predicate avoids module-level side effects.**
``_entry_point_loads_to_component`` now consults
``importlib.util.find_spec`` first and only falls through to
``ep.load()`` when the spec lookup is insufficient. The
``except BaseException`` was narrowed to ``except Exception`` so
``SystemExit`` / ``KeyboardInterrupt`` are no longer swallowed at
filter time.
- **Frontend reload-success warnings surfaced.** The reload route's
``ReloadResult.warnings`` (non-empty on success) now reach the
user via a notice toast in addition to the green success toast.
Wire shape unchanged; this is a UI fix that consumes existing
payload fields.
- **Internal-only file split.** ``sys.modules`` surgery primitives
moved to ``lfx.extension.reload_swap``; ``load_installed_extensions``
/ ``load_seed_extensions`` moved to
``lfx.extension.loader._startup``. Both are re-exported from
their previous import paths so external imports are unchanged.
- **Editable installs are discovered via the entry-point fallback.**
``_distribution_manifest_path`` now falls back to the
``langflow.extensions`` entry-point group when ``dist.files`` only
surfaces ``dist-info/`` entries (the ``pip install -e`` /
``uv pip install -e`` case). The entry-point value is resolved
via ``importlib.util.find_spec`` -- which runs import-system
finders but never executes the module body -- and the resulting
package directory is scanned for ``extension.json`` or a
``[tool.langflow.extension]`` pyproject. Wheel installs are
unaffected: the fallback only fires when the primary ``dist.files``
scan finds no manifest. Previously, editable-installed bundles
were silently dropped by ``lfx extension list`` and the registry,
even though the bundle pyproject already declared the
entry-point.
- **Reload CLI: ``--bundle`` is optional; ``--all`` is implemented.**
``lfx extension reload <ext_id>`` now resolves the bundle name
from local ``discover_all_extensions`` when ``--bundle`` is
omitted; explicit ``--bundle`` still wins for cases where the
local install is not visible to the running server.
``lfx extension reload --all`` iterates every locally-discovered
bundle, POSTs reload to each, and exits non-zero if any reload
fails (previously hard-errored as "not yet wired"). ``--all`` is
mutually exclusive with a positional id / ``--bundle`` (exit 2).
The HTTP wire contract (``POST /api/v1/extensions/{id}/bundles/
{name}/reload`` per-bundle) is unchanged; this is a CLI-only
surface change.
- **User-scoped extension events.** Bundle lifecycle events
(``bundle_reloaded``, ``bundle_reload_failed``, ``flow_migrated``,
``extension_error``) now publish to a per-user keyspace
(``user:<user_id>``) instead of the shared ``"global"`` bucket so
flow-migration and reload payloads cannot leak across users via the
poll endpoint.
- ``reload_bundle`` gains an optional keyword-only ``user_id: str |
None = None`` argument. When supplied, ``bundle_reloaded`` /
``bundle_reload_failed`` events are emitted to keyspace
``user:<user_id>``; ``None`` (CLI / authless dev) keeps the legacy
``"global"`` emission. Existing positional callers are unaffected.
- ``POST /api/v1/extensions/{id}/bundles/{name}/reload`` now resolves
the authenticated user and threads its id into ``reload_bundle``, so
every reload triggered via HTTP is published to that user's
keyspace. Wire contract (status codes, body shape) unchanged.
- ``GET /api/v1/extensions/events`` drops its client-supplied
``keyspace`` query parameter. The endpoint derives the keyspace
from the authenticated user server-side, so an authenticated client
can no longer poll another user's keyspace. Frontends that polled
without ``keyspace`` (the in-tree consumer) are unaffected;
third-party callers that explicitly passed ``keyspace=...`` will
now receive ``422`` from FastAPI's strict parameter validation.
- **Reload event payload aligned with ``ReloadResult``.** Both
``bundle_reloaded`` and ``bundle_reload_failed`` events now carry the
full ``ReloadResult.to_dict()`` envelope (``ok``, ``bundle``,
``reload_id``, ``components_added``, ``components_removed``,
``components_changed``, ``warnings``, ``errors``) instead of a
hand-rolled subset. Polling clients can now (a) detect body-only
edits via ``components_changed`` instead of mis-reporting them as
"no source changes detected", and (b) surface a failed reload's
``errors[0].message`` instead of degrading to a generic
"check server logs" fallback. HTTP response shape unchanged.
- **``GET /api/v1/extensions/events`` rejects ``keyspace`` explicitly.**
Previously the endpoint accepted but silently ignored any
client-supplied ``keyspace`` query parameter (server-derived from the
authenticated user since the prior entry). Silent drop masked client
bugs that assumed the value had effect. The route now returns ``422
Unprocessable Entity`` with a typed
``extension-events-keyspace-forbidden`` error envelope when the
parameter is present. ``extension-events-keyspace-forbidden`` is
added to ``ERROR_CODES`` (additive; codes-as-contract semantics
preserved). In-tree polling clients that never sent the parameter
are unaffected.
- **Manifest-less `lfx.bundles` discovery (metapackage split, 1.11).** A
distribution may declare `[project.entry-points."lfx.bundles"]` whose
value is an importable package; each immediate subdirectory is loaded as
one bundle at the `@official` slot with **no `extension.json`** (the
langchain-community model; these directories are not inputs to
`lfx extension validate` -- the validator requires an `extension.json` or
`[tool.langflow.extension]` manifest and reports `manifest-not-found`
without one).
`load_lfx_bundles_extensions` is exported from `lfx.extension` (additive).
Discovery precedence for cross-source bundle-name collisions becomes
`installed > seed > lfx_bundles > dev > inline` -- **manifest always
wins**, so a manifest-shipping `lfx-<provider>` shadows the same-named
provider in a metapackage with the existing `bundle-shadowed` warning
(graduation requires no lockstep release). A metapackage provider whose
name is already claimed by an installed or seed source is **never
imported** -- all `@official` sources share the
`_lfx_ext.official.<bundle>.*` sys.modules namespace, so importing the
losing copy would overwrite the winner's live modules; the skipped copy
carries the same typed `bundle-shadowed` diagnostic (on `errors`, matching
the resolver) so filtering by code never mixes severities. Namespace
packages are walked across **all portions**, and resolved roots are
deduplicated by path so duplicate declarations never self-shadow. New
warning-only codes added to `ERROR_CODES` (additive), one per discovery
failure mode and none of which abort startup: `bundle-discovery-malformed`
(declaration does not resolve to an importable package directory --
including a parent package whose `__init__` raises during `find_spec`),
`bundles-provider-name-invalid` (provider folder is not a valid bundle
name), `bundles-root-unreadable` (root cannot be enumerated), and
`duplicate-lfx-bundles-provider` (same provider name in more than one
root; first wins). Manifest-less bundles bypass the
`version-constraint-unsatisfied` API-version gate by construction (no
manifest to carry `lfx.compat`); install-time compatibility rides on the
metapackage's PEP 508 `lfx>=X,<Y` pin instead. Manifest-less records
register with `manifestless=True` (additive field on `LoadResult` /
`BundleRecord`) and are **not hot-reloadable**: the reload pipeline
refuses them with the new typed code `reload-manifestless-unsupported`
(additive in `ERROR_CODES`) instead of failing `manifest-not-found`;
pick up metapackage changes by upgrading the distribution and
restarting the process.
- **`import_mod` promoted to a stable public home.** The lazy-import helper
that bundle packages call from their `__getattr__`-based `__init__.py`
files moved from the internal `lfx.components._importing` to
`lfx.utils.lazy_import` and is now part of the BUNDLE_API surface (name,
signature, and semantics contract-stable). `lfx.components._importing`
re-exports it unchanged, so existing in-tree and third-party callers are
unaffected (additive).
- **Validator accepts inherited entry-points.** `validate_extension` no
longer emits `build-method-missing` for a Component subclass whose base
is a *derived* Component base (name ends with `Component`, e.g.
`LCVectorStoreComponent` / `LCToolComponent`): such classes inherit the
class-level `outputs` declaration and only override the output method,
which the AST-only check cannot resolve across modules. Direct
subclasses of the root `Component` keep the strict inline
`build`-or-`outputs` requirement. Surfaced by the partner graduations
(`lfx-datastax` et al.); previously-passing bundles are unaffected
(strictly fewer false positives).