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. 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
## Changelogentry describing the change. The CI guardscripts/migrate/check_bundle_api_changelog.pyenforces this. Pure-internal refactors that preserve every public symbol's name and signature do not require a changelog entry, but reviewers should be skeptical.
| 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 |
| 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 |
ConnectionInput (legacy Composio connection flow) |
lfx.inputs |
ConnectionRefInput (portable host-resolved reference) |
lfx.io |
| Symbol | Source |
|---|---|
ConnectionRef / ResolvedCredential / CredentialLease |
lfx.integrations |
ConnectionResolutionRequest / ConnectionStatus |
lfx.integrations |
IntegrationError and typed subclasses / INTEGRATION_ERROR_CODES |
lfx.integrations |
normalize_integration_error() / register_error_normalizer() |
lfx.integrations |
IntegrationProvider / OAuthProfile / IntegrationCapability / ScopeSet |
lfx.integrations |
McpToolPin (pinned MCP action contract on a capability) |
lfx.integrations |
integration_action() |
lfx.integrations |
Component.resolve_connection(field_name) |
lfx.custom.custom_component.component.Component |
Component.select_integration_capabilities(capability_ids) |
lfx.custom.custom_component.component.Component |
BaseConnectionResolverService, ConnectionAccessPolicy |
lfx.services.connection |
| Symbol | Source |
|---|---|
BaseTriggerComponent (trigger_kind, provider, needs_connection, trigger_config(), build_event()) |
lfx.base.triggers.base |
TRIGGER_EVENT_FIELD (the input the server writes the firing event into) |
lfx.base.triggers.base |
TriggerDefinition |
lfx.base.triggers.base |
| Symbol | Source |
|---|---|
MCPPresetComponent (_mcp_server_config, _pinned_spec, _get_tools, run_tool) |
lfx.base.mcp.preset |
preset_control_inputs() |
lfx.base.mcp.preset |
PinnedServerSpec / PinnedToolSpec / PinnedToolDiff |
lfx.base.mcp.pinned |
pinned_spec_from_capabilities() / tools_list_digest() / enforce_pinned_tools() |
lfx.base.mcp.pinned |
| Symbol | Source |
|---|---|
Output |
lfx.io |
| Symbol | Source |
|---|---|
Data |
lfx.schema.data |
DataFrame |
lfx.schema.dataframe |
Message |
lfx.schema.message |
| Symbol | Source |
|---|---|
import_mod(attr_name, module_name, package) |
lfx.utils.lazy_import (canonical; lfx.components._importing re-exports it for backward compatibility) |
| Symbol | Source |
|---|---|
Manifest schema (extension.json / [tool.langflow.extension]) |
lfx.extension.manifest.ExtensionManifest |
BundleRef (one entry in optional bundles[]; bundle names must be unique) |
lfx.extension.manifest.BundleRef |
ProviderManifestEntry (one entry in the optional providers[]) |
lfx.extension.manifest.ProviderManifestEntry |
IntegrationManifestRef entries in optional integrations[] |
lfx.extension.manifest.IntegrationManifestRef |
IntegrationCapabilityManifest (bundle-owned versioned provider catalog) |
lfx.integrations.IntegrationCapabilityManifest |
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>.
| Symbol | Source |
|---|---|
load_extension(root, *, bundle_name=None) |
lfx.extension.loader |
load_extension_bundles(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) |
LoadedIntegration |
lfx.extension.loader (validated capability metadata for discovery and policy) |
LoadResult |
lfx.extension.loader |
SLOT_OFFICIAL / SLOT_EXTRA |
lfx.extension.loader |
| Symbol | Source |
|---|---|
reload_bundle(registry, bundle_name) |
lfx.extension.reload |
BundleRegistry (list_components(), list_integrations()) |
lfx.extension.bundle_registry |
BundleRecord (components plus validated integration metadata) |
lfx.extension.bundle_registry |
ReloadInProgressError |
lfx.extension.bundle_registry |
POST /api/v1/extensions/{id}/bundles/{name}/reload |
langflow.api.v1.extensions |
| 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.
| 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 |
| 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 |
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 factoriesroutes— bundle-mounted HTTP routeshooks— bundle-declared lifecycle hooksstarter_projects— bundle-shipped starter flowsuserConfig— bundle-declared user-config schema- Multi-bundle manifests (
bundleslist with length > 1)
The shipped LE-1023 pilot is duckduckgo, extracted into the
standalone distribution
lfx-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
Componentbase class (noLCToolComponentlegacy). - 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 bycheck_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;
the deserialize half is covered by
src/lfx/tests/integration/extension/test_pilot_duckduckgo_upgrade.py.
-
Portable extension paths. Bundle and integration capability-manifest paths reject absolute, drive-qualified, and parent-traversal paths using both Windows and POSIX syntax, independent of the host platform. Editable-install discovery converts PEP 610 file URLs to native paths, including escaped spaces and Windows drive letters. Public signatures and
BUNDLE_API_VERSIONremain unchanged. -
Knowledge storage provider defaults.
DBProviderInputdefaults to the application-provided SQLite backend. Its public name, fields and signature remain unchanged, soBUNDLE_API_VERSIONremains1. Recognized historical Knowledge and Memory sources resolve to current implementations without loading the retired Chroma SDKs. Custom component source remains untouched. Extension backends that implement onlyadd_documentsretain their guarded ingestion path. Writing precomputed embeddings remains optional for ordinary ingestion and required for migration imports.
Graph outputs and the Read File tool now await @delegates_to coroutine methods
directly. BaseFileComponent provides aload_files_* and aprocess_files for
native async loaders; existing sync overrides still run in worker threads. Bundle
coroutines should await these methods instead of calling their sync wrappers on
the event loop. The public sync method signatures remain unchanged, and
BUNDLE_API_VERSION remains 1.
ConnectionNotAuthorizedError'sreasonadditionally acceptslistener-only: the connection holds a credential that only a trigger listener process may resolve. A Slack app-level token (xapp-) opens Socket Mode sockets and has no other use, so the host resolver refuses it everywhere else, including in a triggered run, and a component is told to choose a different connection. The error code and HTTP 403 status are unchanged. This is additive;BUNDLE_API_VERSIONremains1.lfx.base.triggersjoins the surface:lfx-slack0.2.0 subclassesBaseTriggerComponentfor itsSlack: On MessageandSlack: On Reactionnodes, so the base class,TRIGGER_EVENT_FIELDandTriggerDefinitionare now a bundle contract andcheck_bundle_api_changelog.pyguards the module. Every lfx 1.13.0 release ships it; nightlies before1.13.0.dev17do not.lfx-slack0.2.0 keeps the line-widelfx>=1.13.0.dev0floor the release plan requires and imports its triggers lazily, so on those nightlies its actions still load and only the two trigger modules report an import error.
filter_plugin_entry_points() and filter_component_entry_points() inspect
installed manifests only when an entry point has an identifiable distribution.
App startup with no legacy plugins avoids scanning installed package files.
Manifest precedence, entry-point ordering, and public signatures are unchanged;
BUNDLE_API_VERSION remains 1.
- Clarify the documented meaning of
ResolvedCredential.identity is None: the resolver does not know the executing identity (the headlessLF_CONNECTION__*wire format has no place to declare one). It no longer reads as "the operator vouched for this token". A capability that must run as one identity has to establish it another way (lfx-slackreads the token's type prefix) or fail closed withconnection-not-authorized. Documentation only: no symbol, signature, or default changes, andBUNDLE_API_VERSIONstays1.
- Add
InvalidRequestError(invalid-request) andResourceNotFoundError(resource-not-found) tolfx.integrationsandINTEGRATION_ERROR_CODES. Both are non-retryable and provide sanitized input/access hints. Bundles can distinguish invalid inputs and missing resources from temporary provider failures and unsupported actions. Existing codes and normalization defaults remain compatible; this additive change retainsBUNDLE_API_VERSION = 1.
-
Add the pure
Component.select_integration_capabilities(capability_ids)hook. Its argument is the tuple of declared capability IDs from a connection input or the loaded manifest'scomponent_ref. An action-picker bundle returns a non-empty subset based on the invocation's current inputs. Empty or undeclared selections fail with HTTP 422 andaction-unsupported, independently of governance policy. The default requires all declared capabilities, so existing single-action components need no change. The hook runs before the output body in graph and sync/async tool execution, after tool arguments are applied, and before creating a credential lease. The resolver receives only the selected IDs. Graph construction using the defaultComponentToolkitchecks its providers; selected-action checks wait until the agent supplies invocation arguments. Custom_get_tools()orto_toolkit()implementations require every declared capability at construction, and their connection leases carry every declared capability. The selection hook cannot narrow an opaque toolset whose returned callables bypass the standard invocation wrappers. Picker option filtering remains bundle-owned; changing the picker alone never authorizes an action. For example:def select_integration_capabilities(self, capability_ids: tuple[str, ...]) -> tuple[str, ...]: return ({"search": "google.drive.search", "delete": "google.drive.delete"}[self.action],)
Use the same action-to-capability mapping to dispatch the output's adapter. Implementations must inspect input values without I/O or side effects; the hook may run more than once. For a component requiring several actions in one invocation, return every capability it will use. This is additive;
BUNDLE_API_VERSIONremains1. -
IntegrationPolicyErroralso implementsIntegrationError, retainingPermissionErrorcompatibility while carryingpolicy-blocked, HTTP 403, an administrator-action hint and owner-only key details. Component policy denials now pass through the normal error-message persistence handler. -
Empty persisted integration ceilings retain unrestricted semantics when a plugin is installed. External deny-all must be explicitly configured; plugins changing semantics must provide an operator-visible migration/configuration step. No automatic Enterprise migration is included here.
- Policy bundle coordinators expose
read_state()to atomically return the immutable revision and source availability. CustomBasePolicyBundleServiceimplementations must supply this method. Integration policy resolution pins both provider and action decisions to that state, including across async plugin hooks, so concurrent publication cannot combine different revisions. - Failure to look up a component's integration identity now stops execution;
Noneis reserved for a successful lookup with no matching capability. Template building clears previous integration identity stamps before lookup, including when registry access fails.
- Component and host resolver gates require metadata for every declared capability when an action deny-list is configured. Missing or unavailable metadata produces a typed policy denial before credential access; an unconfigured installation retains pass-through behavior.
- Integration policy snapshots support dotted provider IDs and expire cached allows when refreshing a configured shared policy fails, including policies that restrict integration actions without a provider ceiling.
- The synchronous and asynchronous
require_integration_actionshelpers accept optionalcapability_ids;policy_keys_for_capabilitiesaccepts optionalrequire_loadedvalidation. These additions do not change previously released Bundle API signatures;BUNDLE_API_VERSIONremains1.
ConnectionNotAuthorizedError.reasonanddetails.reasonidentify the denial. The optionalreasonargument additionally acceptsanonymous-principal,unknown-principal, andnon-interactive-opt-in-required, each with a safe, actionable hint. The existingprincipalandproviderreasons, error code, and HTTP 403 status are preserved. Owner/share authorization is checked before reporting a missing opt-in; credentials are never read for a denied request. This is additive;BUNDLE_API_VERSIONremains1.
- The bundle registry rejects duplicate integration provider IDs, capability IDs,
and policy keys across providers or bundles with
integration-identity-conflict. Policy keys may group actions within one provider. Startup reports rejected bundles, and reload validates before swapping modules or registry metadata. - Integration references require
lfx>=1.13.0.dev0. The loader and validator reportlfx-version-too-oldbefore schema parsing on older runtimes. CI checks every declaring bundle's runtime dependency against the same feature floor;scripts/ci/sync_bundle_lfx_pin.py 1.13.0remains the floor update mechanism. Manifests without integrations retain their existing behavior.
-
Integration policy gating of bundle capabilities (additive).
IntegrationPolicyBlockedError(codepolicy-blocked, HTTP 403) is added to the typed integration error envelope and toINTEGRATION_ERROR_CODES.IntegrationCapability.policy_keysis now validated against theintegrations.<provider_id>.<action>grammar andIntegrationProviderrejects capability policy keys outside its own provider prefix, so every declared action is addressable by an operator deny-list.ConnectionResolutionRequest.capability_idscarries the capability ids declared by the requesting input, letting a host resolver enforce the deny-list before it selects a credential row.Componentgainsrequire_integration_policy(), called frombuild_results(), so a blocked provider or action fails before the component body runs and before any credential is minted, decrypted, or refreshed. Bundles that declare no integrations are unaffected;BUNDLE_API_VERSIONremains1. -
Owner-only route families on the execution principal (additive).
ExecutionPrincipal.allow_explicit_sharesis a new field defaulting toTrue, so every family that already honored explicit shares keeps doing so. Route families whose admission never admits a delegated caller (the legacy MCP transports) stamp itFalse, and a host resolver that evaluates share grants must skip its share branch for those principals. The portable deny floor inBaseConnectionResolverService.authorize_principalis unchanged in behavior: it admits a share only when the host passesexplicit_share_authorized, and its docstring now states that a host must never authorize a share for a principal with this flag set toFalse. Additive for bundles and resolvers alike;BUNDLE_API_VERSIONremains1. -
2026-09-05 (
lfx-microsoft). First consumer of the bundle-owned integration manifest:lfx-microsoftships eight Microsoft Graph delegated actions and builds itsConnectionRefInputscopes from its owncapabilities.v1.json. No surface in this document changed --BUNDLE_API_VERSIONremains1-- the entry is recorded so the contract's history names its first out-of-tree consumer. -
Optional rejected-token digest for connection refresh.
ConnectionResolutionRequest.rejected_token_digestcarries a SHA-256 digest only after a provider rejects a cached credential.CredentialLeasesupplies it on its single reactive retry so a host can coordinate replacement across workers without transferring token material. Existing request construction and resolver implementations remain compatible;BUNDLE_API_VERSIONremains1. -
Bundle-owned integration capability manifests (additive).
ExtensionManifest.integrations[]now carriesIntegrationManifestRefvalues (provider_id, owningbundle, relative JSONpath). The referencedIntegrationCapabilityManifestis versioned withschema_version=1and declares authentication profiles plus executable actions, required and conditional scopes, policy keys, substrate, maturity, deployment contexts, risk, and execution targets. The loader validates bundle ownership and provider identity and exposes the parsed catalog asLoadedIntegrationinLoadResult.integrationsand retains it inBundleRecord.integrationsfor process-wide discovery and policy reads throughBundleRegistry.list_integrations(). Manifests that omitintegrationsstill load with an empty list;BUNDLE_API_VERSIONremains1. Capability paths require a lowercase.jsonsuffix in both the runtime validator and exported schema. Multi-bundle loading registers extension-wide model providers only after every bundle validates and loads successfully. -
Enforced the unreleased connection resolver contract through a final
resolveentry point. Hosts now implement_get_access_policyand_resolve; ownership and non-interactive/share checks run before credential access. Required scopes reject unverified credentials with a typed diagnostic. Resolution failures carry fixed reason codes and actionable guidance without raw credential values or exception chains.run_flownow activates its injected variables and environment policy for credential lookups, restoring the prior scope after execution. No previously released API changes;BUNDLE_API_VERSIONremains 1. -
Added the
credential-undecryptableConnectionUnresolvedReason, raised when a stored credential exists but cannot be decrypted (for example after the server secret key changed). It keeps that case distinct from a missing credential, and its guidance tells the user to reconnect. Code that matches on reason values should handle the new value. Additive to the unreleased connection contract;BUNDLE_API_VERSIONremains 1. -
Initial surface enumerated above. Frozen as
BUNDLE_API_VERSION = 1. -
Added the provider-neutral connection-reference, resolver, capability, integration-error, and telemetry contracts used by dedicated integration bundles. This is an additive surface change and does not change
BUNDLE_API_VERSION. -
Hardened the new connection contracts before their first release: integration exports load lazily;
ScopeSet.coversrequires an explicitproviderand shares normalization with resolvers throughScopeSet.missing; leases activate conditional scopes and support pre-run construction. Malformed credentials produce sanitized typed errors; wrapped HTTP failures preserve their status, and ambiguous 403s no longer claim missing scopes. The authorization floor accepts an optional host-verifiedexplicit_share_authorizeddecision for actor ownership mismatches without bypassing other denies. No previously released Bundle API signature changes;BUNDLE_API_VERSIONremains 1. -
ExtensionManifest.versionnow accepts the canonical PEP 440 stable, dev, alpha, beta, and release-candidate forms emitted by the repository's bundle release pipeline, in addition to the existing SemVer 2.0.0 forms. Runtime and published JSON Schema validation remain equivalent; this is additive and does not changeBUNDLE_API_VERSION. -
Typed loader diagnostics now distinguish unavailable optional providers from broken bundle imports.
optional-dependency-missingis added toERROR_CODES; a manifest-lesslfx-bundlesmodule emits this warning only when its declared provider distribution is genuinely absent, allowing the remaining bundle components to load. Undeclared imports and installed but broken distributions remainmodule-import-failederrors. Expectedbundle-shadowed/seed-bundle-shadowedprecedence outcomes are warnings; winner selection is unchanged. -
Extension manifests may now declare multiple component bundles. Startup discovery loads every declared bundle through
load_extension_bundles(); direct callers may select one withload_extension(..., bundle_name=...). Single-bundle and provider-only manifests retain their existing behavior, so this is additive and does not changeBUNDLE_API_VERSION. -
Inline bundle discovery now skips Langflow-owned component compatibility shims marked with
# lfx-bundles-shimor# lfx-compat-shim. These packages remain importable, but are not treated as user inline bundles and no longer emitbundle-empty. -
BundleRegistry.write_locked()exposed as a public context manager so the reload pipeline can hold the registry write lock across both thesys.modulesswap and theBundleRecordinstall. 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) returns422 Unprocessable Entityfor structural failures (broken bundle, missing source path, name mismatch) instead of200 OKwithok=false. Body is{...primaryError, result: ReloadResult}so the full typed result is preserved under the FastAPIdetailenvelope.409 Conflictforreload-in-progressis unchanged. -
CLI table updated to remove the obsolete
dev register/dev unregister/dev listsubcommands; the actual surface isextension dev <path>plus the Python helpersregister_dev_extension/unregister_dev_extension. -
MigrationTable.ambiguous_bare_namesadded. Each entry is{name, candidates: [list of canonical IDs]}and registers a bare class name that exists in 2+ bundles. The deserializer now surfacescomponent-name-ambiguous(with the candidate targets) for any bare name listed here, instead of falling through to the genericcomponent-not-found-with-hint. Seeded with the canonical regression cases (MergeDataComponent,SplitTextComponent,SubFlowComponent).check_bare_names.pynow 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
.pyundersrc/backend/base/langflow/api/**andsrc/lfx/src/lfx/**; a new file that mounts anAPIRouter(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-scopemarker. -
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.pynow comparesambiguous_bare_namesalongsideentries. A marker may not be removed once published, and itscandidateslist may only grow -- shrinking it would silently regress flows fromcomponent-name-ambiguoustocomponent-not-found-with-hint. -
Router-trust guard now resolves dotted attribute references in
include_routerand decorators.include_router(child.api.router, prefix="/extensions")afterimport child.api(and theimport child.api as alias; alias.routershape) are caught -- not justfrom child.api import router as child_router. The parser flattens anyName/Attributechain, and the resolver walks imports of either kind (from M import Nandimport 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 Yanchors at the package itself (level=1 ->pkg); inside a regular modulepkg.fooit anchors at the parent package (level=1 ->pkg). The arithmetic differs because__init__.py's file module IS the package, whilepkg/foo.py's file module ispkg.foo. The resolver tracksis_packageand decrementslevelby one for__init__.pyfiles 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.
DiscoveredExtensionrecords emitted fromdiscover_installed_extensions/discover_seed_extensionsnow run the same resolve-and-relative_tocontainment check thatvalidate_extensionperforms. A symlinkedbundles[0].pathor a symlinked seed subdirectory that escapes the extension root is now rejected withpath-escapebefore reaching the loader, instead of slipping through toexec_module(). The shared primitive lives atlfx.extension._paths.is_within; every walker (loader, validator, seed discovery, inline-bundle discovery) uses the same function and the sameSKIP_DIR_NAMES. --execute-importsenv allowlist. The validator's--execute-importssubprocess now inherits an explicit allowlist (PATH,LANG,LC_*,SYSTEMROOT,TMPDIR,TZ, Python locale + encoding vars) instead of denylisting onlyLANGFLOW_*/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_ionow flagsexec,eval,__import__,compileas top-level primitives andimportlib.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_modulesnow builds the staging->prod rename map before anysys.modulesmutation, snapshots popped old modules into a recovery map, and restores them on any mid-swap exception. The length-mismatch tripwire onzip(strict=True)no longer leaves the prod namespace shredded. A new typed code,reload-class-retag-failed, is appended toReloadResult.warningswhencls.__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_extensionsnow detects two distributions with different canonical names but identicalbundle.name(which would silently clobber each other at_lfx_ext.official.<name>.*) and emits a typedduplicate-bundle-nameerror on the loser, dropping its components.BundleRegistry.install_bundleadditionally logs a WARNING when an existing record is replaced by a record from a differentsource_path(catches collisions the upstream precedence resolver missed). - Reload endpoint off event loop.
POST /api/v1/extensions/{id}/bundles/{name}/reloadnow invokesreload_bundleviaasyncio.to_threadso 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-milestoneis renamed to the stablemulti-bundle-unsupported. The old code is retained inERROR_CODESas a deprecated alias for one milestone for log scrapers. Three new codes are added toERROR_CODES:duplicate-bundle-name(see above),reload-class-retag-failed(see above), andreload-transport-error(CLI-side connectivity failure, previously misreported asreload-source-missing). - Discovery preserves "unreadable" vs "absent" distinction.
_pyproject_declares_extensionnow propagatesOSErrorso a permission failure on a pyproject that might declare an extension surfaces asmanifest-unreadableinstead of being silently dropped as "no extension here". - Dev registry corruption is logged.
_read_statenow 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 nextlangflow run. - Entry-point predicate avoids module-level side effects.
_entry_point_loads_to_componentnow consultsimportlib.util.find_specfirst and only falls through toep.load()when the spec lookup is insufficient. Theexcept BaseExceptionwas narrowed toexcept ExceptionsoSystemExit/KeyboardInterruptare 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.modulessurgery primitives moved tolfx.extension.reload_swap;load_installed_extensions/load_seed_extensionsmoved tolfx.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_pathnow falls back to thelangflow.extensionsentry-point group whendist.filesonly surfacesdist-info/entries (thepip install -e/uv pip install -ecase). The entry-point value is resolved viaimportlib.util.find_spec-- which runs import-system finders but never executes the module body -- and the resulting package directory is scanned forextension.jsonor a[tool.langflow.extension]pyproject. Wheel installs are unaffected: the fallback only fires when the primarydist.filesscan finds no manifest. Previously, editable-installed bundles were silently dropped bylfx extension listand the registry, even though the bundle pyproject already declared the entry-point. - Reload CLI:
--bundleis optional;--allis implemented.lfx extension reload <ext_id>now resolves the bundle name from localdiscover_all_extensionswhen--bundleis omitted; explicit--bundlestill wins for cases where the local install is not visible to the running server.lfx extension reload --alliterates every locally-discovered bundle, POSTs reload to each, and exits non-zero if any reload fails (previously hard-errored as "not yet wired").--allis mutually exclusive with a positional id /--bundle(exit 2). The HTTP wire contract (POST /api/v1/extensions/{id}/bundles/ {name}/reloadper-bundle) is unchanged; this is a CLI-only surface change.
- Path-safety contract honored on every discovery path.
-
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_bundlegains an optional keyword-onlyuser_id: str | None = Noneargument. When supplied,bundle_reloaded/bundle_reload_failedevents are emitted to keyspaceuser:<user_id>;None(CLI / authless dev) keeps the legacy"global"emission. Existing positional callers are unaffected.POST /api/v1/extensions/{id}/bundles/{name}/reloadnow resolves the authenticated user and threads its id intoreload_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/eventsdrops its client-suppliedkeyspacequery 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 withoutkeyspace(the in-tree consumer) are unaffected; third-party callers that explicitly passedkeyspace=...will now receive422from FastAPI's strict parameter validation.
-
Reload event payload aligned with
ReloadResult. Bothbundle_reloadedandbundle_reload_failedevents now carry the fullReloadResult.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 viacomponents_changedinstead of mis-reporting them as "no source changes detected", and (b) surface a failed reload'serrors[0].messageinstead of degrading to a generic "check server logs" fallback. HTTP response shape unchanged. -
GET /api/v1/extensions/eventsrejectskeyspaceexplicitly. Previously the endpoint accepted but silently ignored any client-suppliedkeyspacequery 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 returns422 Unprocessable Entitywith a typedextension-events-keyspace-forbiddenerror envelope when the parameter is present.extension-events-keyspace-forbiddenis added toERROR_CODES(additive; codes-as-contract semantics preserved). In-tree polling clients that never sent the parameter are unaffected. -
Manifest-less
lfx.bundlesdiscovery (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@officialslot with noextension.json(the langchain-community model; these directories are not inputs tolfx extension validate-- the validator requires anextension.jsonor[tool.langflow.extension]manifest and reportsmanifest-not-foundwithout one).load_lfx_bundles_extensionsis exported fromlfx.extension(additive). Discovery precedence for cross-source bundle-name collisions becomesinstalled > seed > lfx_bundles > dev > inline-- manifest always wins, so a manifest-shippinglfx-<provider>shadows the same-named provider in a metapackage with the existingbundle-shadowedwarning (graduation requires no lockstep release). A metapackage provider whose name is already claimed by an installed or seed source is never imported -- all@officialsources 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 typedbundle-shadoweddiagnostic (onerrors, 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. This precedence is a startup-time property:load_lfx_bundles_extensionsruns in the component-loading path, not indiscover_all_extensions, solfx extension list-- which walks only the installed and seed (manifest-bearing) sources -- does not enumerate manifest-less metapackage providers. A livelfx.bundlesprovider is therefore invisible at list-time, and its shadowing against a manifest-shipping package is resolved only when the server assembles the_lfx_ext.official.<bundle>.*namespace at startup. New warning-only codes added toERROR_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 duringfind_spec),bundles-provider-name-invalid(provider folder is not a valid bundle name),bundles-root-unreadable(root cannot be enumerated), andduplicate-lfx-bundles-provider(same provider name in more than one root; first wins). Manifest-less bundles bypass theversion-constraint-unsatisfiedAPI-version gate by construction (no manifest to carrylfx.compat); install-time compatibility rides on the metapackage's PEP 508lfx>=X,<Ypin instead. Manifest-less records register withmanifestless=True(additive field onLoadResult/BundleRecord) and are not hot-reloadable: the reload pipeline refuses them with the new typed codereload-manifestless-unsupported(additive inERROR_CODES) instead of failingmanifest-not-found; pick up metapackage changes by upgrading the distribution and restarting the process. -
import_modpromoted to a stable public home. The lazy-import helper that bundle packages call from their__getattr__-based__init__.pyfiles moved from the internallfx.components._importingtolfx.utils.lazy_importand is now part of the BUNDLE_API surface (name, signature, and semantics contract-stable).lfx.components._importingre-exports it unchanged, so existing in-tree and third-party callers are unaffected (additive). -
Validator accepts inherited entry-points.
validate_extensionno longer emitsbuild-method-missingfor a Component subclass whose base is a derived Component base (name ends withComponent, e.g.LCVectorStoreComponent/LCToolComponent): such classes inherit the class-leveloutputsdeclaration and only override the output method, which the AST-only check cannot resolve across modules. Direct subclasses of the rootComponentkeep the strict inlinebuild-or-outputsrequirement. Surfaced by the partner graduations (lfx-datastaxet al.); previously-passing bundles are unaffected (strictly fewer false positives). -
Manifest
providers[]— bundles can register model providers (additive).ExtensionManifestgains an optionalproviders[]list (ProviderManifestEntry). Each entry declares a model provider — name, metadata (icon, credential variables,mapping.model_class), optional lazymodel_class/embeddingimport refs,api_key_required,live/conditional_live, and dotted-pathlive_discovery/validatorcallables — which the loader merges into lfx's unified model system at load time vialfx.base.models.provider_registry. Built-in providers always win on a name collision.bundles[]is now 0-or-1 rather than exactly one, so a provider-only extension may ship providers with no component bundle; an extension must declare at least one ofbundlesorproviders. Existing single-bundle manifests are unaffected. Discovery (discover_installed_extensions/discover_seed_extensions) surfaces a provider-only extension withDiscoveredExtension.bundle_name = None, andregistry.Extension.bundle_nameis likewise nowstr | None(thelfx extension listBUNDLE column shows—for such extensions). -
Provider base-URL suffixes (additive). A provider metadata variable whose
langchain_paramisbase_urlmay declarebase_url_suffix(for example,/v1). Runtime chat and embedding clients append the normalized suffix only when the configured URL does not already end with it. The field is published in the extension JSON Schema, and unrecognized sibling keys on base-URL variables are rejected so misspellings cannot silently disable normalization. -
Provider identity and static catalogs (additive). A
providers[]entry may now declare a stable lowercaseprovider_id, an independentdisplay_name, legacyaliases, and a dotted-pathcatalog_loader. The loader must return a flat list of model metadata rows; Langflow validates model identities and stamps provider ownership before merging those rows into the unified catalog. Manifests that omitprovider_idretain their existing behavior through a deterministic ID derived fromname.ProviderDescriptoris the preferred public registry type andProviderSpecremains an alias for source compatibility. The registry also exposes an immutable generation-tagged snapshot plus an eager catalog validation hook for deployment readiness. Existing provider manifests are unaffected andBUNDLE_API_VERSIONremains1. -
Provider identity resolution and policy snapshots (additive).
resolve_provider_id()is the canonical resolver for registered names, display names, aliases, stable IDs, and deterministic legacy fallbacks. Model-provider policy services now expose cached synchronousresolve(), asyncaresolve(), single-provideris_allowed(), andinvalidate()hooks while preserving the existing immutable snapshot and allow-all OSS behavior. The process-local snapshot cache has a bounded TTL, and async implementations can overrideaget_allowed_provider_ids()without bypassing it. Existing synchronous policy subclasses, including subclasses that did not callsuper().__init__(), remain source-compatible. -
New typed error codes (additive):
provider-invalid,provider-skipped. A malformed provider spec surfacesprovider-invalid; a provider whose name collides with a built-in or already-loaded provider surfacesprovider-skipped. Both are warning-only — the rest of the extension still loads — so adding them is backward-compatible (noBUNDLE_API_VERSIONbump). -
ExtensionError.ref_url/ CLIsee:no longer carry per-code anchors. Auto-derivedref_urlvalues (and the renderedsee:line) now point to the single extension-error guidance page (https://docs.langflow.org/extensions/errors) instead of…/extensions/errors#<code>. Thecode,message,location,content, andhintfields are unchanged; only the docs link shape changed. Callers that deep-linked onref_urlsuffix should readcodedirectly instead. -
duplicate-distributiondetection resolves symlinks first.load_installed_extensionscollapses manifest paths that are merely different spellings of the same physical file (compared viaPath.resolve(); anOSErrorduring resolution falls back to the raw path) before the duplicate check. RHEL-family (ubi) venvs symlinklib64 -> liband put both spellings onsys.path, soimportlib.metadata.distributions()yields every installed distribution twice — previously each manifest-shipping bundle logged a falseduplicate-distributionerror at Docker startup. The lexicographically-first unresolved spelling remains the winner, so error messages and winner selection are unchanged, and two physically distinct manifests for one canonical name still error. No public symbol's name or signature changed. -
ResolvedCredential.identity(additive, optional).lfx.integrations.models.ResolvedCredentialgainedidentity: Literal["user_delegated", "bot", "service"] | None = None, mirroringlfx.integrations.capabilities.IntegrationIdentity. The database-backed resolver populates it from the connection row'sexecuting_identity; the headless environment resolver leaves itNonebecause theLF_CONNECTION__*wire format has no place to declare one. Providers whose user and bot tokens share scope names — Slack'schat:writeis both a User Token Scope and a Bot Token Scope — cannot distinguish the two identities fromgranted_scopes, so a bundle capability that must run as a bot compares this field and fails closed withconnection-not-authorizedbefore its first request. The field defaults toNone, no existing field changed name, type, or meaning, and every existing construction site keeps working, soBUNDLE_API_VERSIONremains1. -
Pinned action-to-tool mode for preset MCP components (additive).
MCPPresetComponentgains a_pinned_spec()hook returning aPinnedServerSpec(lfx.base.mcp.pinned); in pinned mode the component fixes the endpoint and transport, refuses any pinned tool that was removed, renamed, or re-shaped relative to the pin, compares the pinned subset'stools/listcontent digest and theInitializeResult.serverInfoname/version when they are pinned, keeps the Tool dropdown off the live server, and re-checks call arguments against the pinned schema on both of a tool's call paths (coroutineandfunc). Only an argument the pin does not declare is treated as drift; an omitted required field is left to the derived args schema, whose error is self-correctable.pinned_spec_from_capabilities()additionally refuses atools_list_hashthat is not the digest of exactly the tools the manifest pins, so that authoring mistake surfaces at build time instead of as runtime "drift". Drift raises the newIncompatibleToolError(incompatible-tool, added toINTEGRATION_ERROR_CODES). Capabilities carry the pin asIntegrationCapability.mcp_pin: McpToolPin; a capability whosesubstrateismcpmust now declare bothmcp_toolandmcp_pin. Tools discovered byupdate_toolscarry their rawinput_schema/output_schemaonmetadata, and a server config may setallow_sse_fallback=Falseto pin the transport. Components that do not override_pinned_spec()are unaffected;BUNDLE_API_VERSIONremains1. Pinned discovery validates the raw tool list before schema conversion can skip entries and rejects duplicate or unnamed tools. Extra unpinned tools may vary with the user's grant: they are informational inPinnedToolDiff.added, do not affectis_compatibleor the pinned digest, and are excluded before conversion from the component's toolset and execution cache. A missing pinned tool's error hint also directs the operator to check the connection's grants. Session reuse separates callers that permit SSE fallback from callers that require Streamable HTTP, so a live legacy session cannot bypass the pin. -
discovered_tool()accepts a recordedtools/listentry.lfx.base.mcp.pinned.discovered_tool()(and thereforetools_list_digest()) now normalizes a plain mapping in addition to a live MCP tool object, so a bundle author can compute a pin'stools_list_hashdirectly from the recordedtools/listresponse that the pin is derived from. Additive;BUNDLE_API_VERSIONremains1.