SIGN IN SIGN UP

chore: back-merge release-1.12.0 into main (fast-forward only) (#14652)

* fix(ci): assert the base wheel's real console script name (forward-port of #14571) (#14582)

fix(ci): assert the base wheel's real console script name (#14571)

The "Base Distribution Wheel" job verified that the base-only environment
exposes a `langflow` console script and no `langflow-base` one. That is
inverted: `langflow-base` declares `langflow-base = langflow.langflow_launcher:main`,
while the `langflow` script belongs to the root `langflow` distribution --
which the same step explicitly forbids from that environment. The assertion
could never pass, and the follow-on boot step invoked `bin/langflow`, which
does not exist there either.

These expectations were carried over from the langflow-core wheel this job
used to test (#14352) and were never re-pointed at langflow-base. Swap both
script assertions, give them failure messages so a future break is not a bare
AssertionError, and boot the server via `bin/langflow-base`, matching
`docker/build_and_push_base.Dockerfile`.

* fix(test): stop the live smoke suite inheriting the loopback server list (#14583)

playwright.live.config.ts built itself with defineConfig(baseConfig, {...}),
expecting its webServer list to replace the base one. Multi-argument
defineConfig concatenates array options instead, so the live run started five
servers rather than two: the loopback OpenAI fixture, the base backend on 7860
pointed at that fixture, the base frontend on 3000, then the live backend on
7861 and a second frontend on 3000. The duplicate port 3000 with
reuseExistingServer:false failed the job before a single test ran.

The port clash was masking the real defect. Without it the suite would have
driven the base frontend, whose proxy targets the 7860 backend running against
the loopback fixture, so every "live provider" assertion would have passed
without reaching a real provider -- exactly the leak the config's comment says
it prevents.

Spreading baseConfig overrides webServer instead of appending to it. Verified
by bundling the real config: 5 webServer entries before (two on port 3000),
2 after (7861 backend, 3000 frontend), with testDir and testIgnore unchanged.

* fix(ci): assert the base console script the 1.12 line actually ships (#14584)

The base-distribution wheel job asserts a `langflow-base` console script, but
on this line the base wheel declares `langflow`:

    # src/backend/base/pyproject.toml
    [project.scripts]
    langflow = "langflow.langflow_launcher:main"

so the job fails with "langflow-base console script missing from
.../base-test-env/bin" and never reaches the boot check.

a2c015a1d2 (#14339) renamed that entry point from `langflow-base` back to
`langflow` on release-1.12.0 only. #14571 was written against a main that
predates the rename, where `langflow-base` was correct, and the release-1.12.0
back-merge kept that assertion alongside the post-rename pyproject -- two
separate files with no textual overlap, so the merge was clean while the pair
was not. #14582 then carried the stale assertion onto release-1.12.0 as well.

Restores the assertion and the boot invocation to `langflow`, and adds a
comment tying both to [project.scripts] so the coupling is visible to the next
person who back-merges across the rename.

Verified by parsing the base pyproject and the workflow in the same tree:
declared ['langflow'] vs asserted ['langflow-base'] before (incoherent),
declared ['langflow'] vs asserted ['langflow'] after (coherent).

* fix(tests): measure the flow persistence barrier from the reload, not before it (release-1.12.0) (#14588)

fix(tests): measure the flow persistence barrier from the reload, not before it (#14587)

Windows Playwright shards 30/70 and 31/70 were the only failing jobs in nightly
run 31867911970; all 70 Linux shards passed, including the Linux shards running
the same spec. Five of bulk-delete-sessions.spec.ts's fourteen tests failed with

    Flow <uuid> did not finish model refresh and autosave persistence within 30000ms

reloadAndWaitForFlowPersistence created its deadline setTimeout before calling
page.reload(), so the 30s budget had to cover the page load as well as the
model refresh and autosave it is actually there to observe. Playwright serves
the editor from a Vite dev server (`npm start`), so a reload replays ~3.5k
unbundled module requests. Measured from the blob-report traces on Windows:

    trace      page.reload()   GET /flows/{id}   POST custom_component/update
    e3fe6e22        19.0s           t+27.6s          t+29.7s (1.06s)
    1a49b9dd        21.5s           t+28.9s          t+47.5s (10.8s)
    f6f421a4        34.9s              --                 --

The third reload outlasts the whole budget on its own, so that run could never
pass. Arm the deadline after the reload resolves and raise it to TIMEOUTS.long;
the worst observed post-reload cost was ~37s, and the test timeout is 5min
while these tests run 65-95s.

The barrier reaches 38 call sites across 30 spec files, so this was a latent
flake for every Windows spec that configures the loopback provider, not just
the two shards that happened to pair two playground chat builds on one runner.

(cherry picked from commit 6e1aadae21594f1cd1c96e7b717517bd3f476030)

* fix(ci): grant the label job pull-requests write (release-1.12.0) (#14591)

fix(ci): grant the label job pull-requests write

Every "Label PR" run has failed since #14540 -- 67 successes and no failures
before it, 16 failures after (the successes since are runs where the job's `if:`
skips it, e.g. merge_group and bot PRs):

    POST /repos/langflow-ai/langflow/issues/14588/labels
    403 Resource not accessible by integration

#14540 added a `permissions:` block to this workflow. Before that there was
none, so it inherited the repository default, which includes pull-requests
write. Labelling a *pull request* needs that scope: the `issues` permission only
covers real issues even though the REST path is `/issues/{n}/labels`. GitHub
says so in the response itself:

    x-accepted-github-permissions: issues=write; pull_requests=write

Also unblocks Namchee/conventional-pr in the same workflow, which cannot post
its report under a read-only pull-requests scope.

(cherry picked from commit 9454ac40d4b5539e6b704cb949649913ed9e8146)

* perf(tests): seed the loopback provider into starter templates instead of reloading (release-1.12.0) (#14593)

perf(tests): seed the loopback provider into starter templates instead of reloading (#14589)

`configureLoopbackOpenAI` patched the persisted flow behind the running editor
and then reloaded the page so the editor would pick the change up. Playwright
serves the app from a Vite dev server, so that reload replays ~3.5k unbundled
module requests: 19-35s on Windows CI, and it happens once per test across 38
call sites.

Nothing forces the configuration to arrive out of band. `useAddFlow` posts the
starter template the browser fetched from `/api/v1/flows/basic_examples/`, so
serving that catalog already pointed at the loopback fixture makes the flow
*born* configured — the editor and the database never diverge and there is
nothing to reload for.

`seedLoopbackProvider(page)` installs that route and must run before the first
navigation, since React Query caches the catalog for the session.
`configureLoopbackOpenAI` then takes a fast path when the flow it reads is
already configured, and keeps the patch-and-reload path otherwise, so a spec
that does not seed (or builds its flow from a blank canvas) is unaffected. The
fallback warns rather than staying silent, so the optimization cannot rot
unnoticed across the seeded specs.

The one thing that can still write these nodes without a reload is the model
refresh `useApplyFlowToCanvas` fires on mount, so the fast path waits for it.
Refreshes carry no flow in their URL — `buildRefreshPayload` stamps
`_frontend_node_flow_id` onto the template — so `modelRefreshFlowId` attributes
them, and the tracker is armed before navigation to avoid a retroactive wait.

The shared mutation and predicates move into `loopback-provider-policy.mjs`
alongside the existing `flow-editor-persistence-policy.mjs`, pure and unit
tested, so the route seeder and the patch path cannot drift apart.

Not rolled out to specs that build from a blank canvas (`decisionFlow`,
`similarity`, `Youtube Analysis`) — seeding the template catalog does nothing
for them. Deliberately opt-in rather than folded into `openStarterProject`:
`live/llm-provider-smoke.spec.ts` uses that helper and must reach a real
provider, which is exactly the failure mode #14540 fixed for the live config.

Measured locally on macOS, bulk-delete-sessions.spec.ts (8 tests, 2 workers):
2.8m before, 1.6m after, all passing both ways. macOS reloads are far cheaper
than the 19-35s measured on Windows, so the CI saving should be larger.

(cherry picked from commit b40b405ec8799fa744b6232d4b6e71b7da75b55c)

* fix(tests): finish the public build before closing the popup; widen the messages loading-state wait (release-1.12.0) (#14596)

fix(tests): finish the public build before closing the popup; widen the messages loading-state wait

Nightly 31907290063 (main @ b40b405e) failed exactly two Playwright shards.

Windows 24/70 - messages.a11y "scans the named loading state": the expect
after `page.goto("/settings/messages")` used the default 5s. The trace shows
goto returning at `load`, then auto_login (1.6-3.0s) -> whoami -> config ->
the lazy settings route; the messages query mounted 7.0s / 7.5s after goto,
1.4s / 1.7s after the expect gave up. The aria snapshot at failure was the
app-level "Loading..." page, not SessionView's status. Use TIMEOUTS.standard,
which the identical held-response loading scan in knowledge-bases.a11y
already uses.

Linux 41/70 - publish-flow: the spec sent a message in the shareable
playground popup and closed it 30ms later, while the public build was still
in flight. Aborting that request mid-write made the backend terminate its
aiosqlite connections under cancellation; the trace + backend log show a
~60s window where every SQLite writer stalled (the un-publish PATCH never
answered, the retry's auto_login hung 34s+, the sibling worker's build took
71s instead of 0.66s) while reads kept answering in ms. Wait for the build
to finish (Stop visible -> hidden via the shared sendPlaygroundMessage
helper) before closing the popup, which also proves the published playground
completes a run rather than merely starting one.

Verified locally against the full Playwright stack: both tests pass.

(cherry picked from commit 517765cb467cad36a2d2608f252aa8d6a9f1385b)

* fix(a11y): keep visible text in Setup Provider button's accessible name (#14569)

The Setup Provider CTA on the Language Model field forwarded the field
label as its sole accessible name (aria-labelledby -> "Language Model
required"), so its visible text "Setup Provider" was absent from the
name. That fails WCAG 2.5.3 (Label in Name, level A): screen reader users
hear the field name rather than the action, and speech-input users
saying "click Setup Provider" match nothing.

Unlike the combobox branch of the same trigger — whose visible text is
its *value*, so the field label alone is the correct name — this branch
is a plain button whose visible text is its label. Compose the button's
own text first and keep the field label after it, so the name leads with
what the user reads while still identifying which field the CTA belongs
to. Same composition for the literal aria-label fallback.

This also restores the stated intent of #14444, whose QA item 7 called
for "the field's label if the button is icon-only, or the visible button
text if present".

* fix(auth): reconcile a group set the user moved past instead of serving it from cache (LE-2099) (#14594)

* fix(auth): reconcile a group set the user moved past instead of serving it from cache (LE-2099)

The LE-2109 reconciliation cache was keyed by the exact directory state it
verified, and entries were only ever added and aged out. A group set that
was cached, then changed, then changed back still matched its earlier entry,
so a promotion followed by an IdP revocation kept the promoted role until
the stale entry expired - up to EXTERNAL_AUTH_GROUP_RECONCILE_INTERVAL_SECONDS
on the replica holding it, and the authorization plugin was never consulted
in that window. QA reproduced it as [devs] -> [admins, devs] -> [devs]:
the last step served admin for 60s.

The cache now holds one entry per user: the last state a confirming pass
verified. A request is skipped only when that entry is fresh and carries the
same state, so any claim that differs from the last reconciled state misses
by construction, including one that was itself cached earlier.

Two more rules cover overlapping passes for one user (an old and a new token
in flight together): a miss drops the user's entry and hands the pass a
ticket that only the latest pass holds, and a pass that changed the stored
state invalidates - dropping the entry and revoking the outstanding ticket -
so a concurrent no-op pass that verified the previous state can neither be
served nor remembered after the change landed.

The settings description already promised "a group set that differs from the
last reconciled one always reconciles immediately"; the implementation now
matches it.

* docs(auth): spell out the one-slot-per-user trade-off in the reconcile cache

* docs: use EmpirioLabs AI consistently (#14251)

Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>

* fix(security): replace eval() with safe Literal construction in schema.py (#14502)

* fix(security): replace eval() with safe Literal construction in schema.py

Replace two eval() calls (Bandit S307) in create_input_schema() and
create_input_schema_from_dict() with Literal[tuple(options)], which is
semantically identical but avoids arbitrary code execution.

The eval() calls constructed Literal types from user-provided dropdown
options via f-string interpolation. In multi-tenant Langflow deployments
where component templates can be user-uploaded, this represents a
potential code injection vector (CWE-95).

Add tests verifying options-based Literal construction works correctly
for both multi-option and single-option cases.

* fix(tests): import get_origin and cover both schema builders

Adds module-level get_origin import so the Literal assertions run without
NameError or Ruff F821, renames the from_dict tests to name the builder
under test, and adds option coverage for create_input_schema() itself:
single/multiple options build Literal types, while over-limit and empty
option lists fall back to the base field type.

* feat: let operators turn off database spans (#14552)

* feat: let operators turn off database spans

Database spans are the bulk of what gets exported. Measured against a live run with a
commercial APM on the other end: about 80% of exported spans were SQLAlchemy, roughly 50
spans per flow run against a single flow.execute. APMs bill per span ingested, so an
operator who turned export on to watch flow health pays mostly for connect and SELECT.

LANGFLOW_OTEL_DB_SPANS=false drops them. On by default, because the volume buys something:
in that same run 17% of pool checkouts took over 50ms and 4% took over 200ms, which is the
difference between knowing a run was slow and knowing it was slow waiting for the database.
Defaulting this off would hide the most common cause of a slow run behind a setting nobody
knows to look for.

Two gates. The instrumentor is not installed at all when it is off, so the spans are never
created rather than created and dropped, and the export scope set drops the scope as well,
which covers another library instrumenting sqlalchemy against our provider.

The setting can only ever subtract from the allowlist. That direction is the point: the
allowlist is what keeps prompt-carrying scopes out of the APM, so configuration must not be
able to widen it, and a test pins that across values including one naming an LLM scope.

Anything but a recognised false value leaves them on, so a typo cannot quietly stop
telemetry an operator believes is running. The doctor reports which way it is set, next to
the existing note about log bodies, since that is where an operator checks what they are
about to send.

* fix: report configuration state in the doctor, and prove instrumentation is skipped

Both from review.

The doctor said database spans "ARE exported". It only reads the setting, and cannot see
whether the instrumentor attached: _instrument_sqlalchemy swallows an ImportError or a
double-instrument call. So the line was a confident claim on exactly the box where it is
wrong. It now says export is enabled, and that the volume follows once instrumentation
attaches.

The disabled-path test only proved that no database spans were exported, which stays true
if the instrumentor attaches and the export filter drops its spans. That is not what the
setting promises: skipping instrumentation is what avoids paying span creation on every
query. Added a probe with a plain SimpleSpanProcessor and no filtering, so any span that
exists at all is visible, plus its positive control.

Verified rather than assumed: with _instrument_sqlalchemy forced to instrument regardless,
the new test is the only one of the 32 that fails.

No mock or spy, so the probe exercises the real instrumentor rather than asserting on a
call that was recorded.

* fix: keep the sqlite file path out of exported database spans (#14559)

* fix: keep the sqlite file path out of exported database spans

SQLAlchemy's instrumentation sets db.name from the database name, and for SQLite that
name is the file path. It then builds db.operation and the span name as
"<operation> <db.name>", so spans arrive at the APM named

    SELECT /home/alice/deployments/langflow-prod/langflow.db

Confirmed present in a commercial APM, not only locally, so it survives export to a
third party. SQLite is the default database, so this is the default configuration
rather than an edge case.

The path is not a credential, but it is host detail the operator did not choose to
send: install directory, the account name on a typical unix path, and whatever their
directory naming gives away. It is also a cardinality problem, because every
deployment path becomes a distinct span name and dashboards do not port between
environments.

Keeps the file name rather than dropping the value, so anyone running more than one
SQLite database can still tell them apart. Postgres and MySQL are untouched: their
db.name is a logical name with no separator, so it is already its own final segment
and the function returns early.

Done at the export boundary, next to the existing URL redaction, because that is
already the one place that decides what leaves and it covers instrumentors the
runtime does not install itself. Splits on both separators, since the host that wrote
the path and the host exporting it need not be the same platform.

* fix: only shorten the database path for sqlite

Found in review. The function shortened any db.name containing a separator, which is a
shape test rather than a system test. A separator is legal inside a Postgres or MySQL
database name, so "tenant/archive" was being exported as "archive" -- silently renaming
a logical database in the operator's dashboards. That is a worse bug than the leak this
function exists to fix, and it contradicted the stated contract that non-file databases
are untouched.

Gated on db.system == "sqlite" instead. Two regression tests, both confirmed to fail
without the gate: a Postgres name with a separator, and a path-shaped value carrying no
db.system at all, where there is no evidence it is a file.

* ci: run backend tests for provider bundle changes (#14606)

The `python` path filter never learned about `src/bundles/**` after the
bundle metapackage split. Since #13614 gated `test-backend` on
`path-filter.outputs.python == 'true'`, a PR that only touches a provider
bundle reports `python=false` and skips the entire backend suite -
including the bundle's own tests under `src/bundles/*/tests/`.

Add `src/bundles/**` to the `python` filter so bundle-only PRs run the
backend suite (and `test-templates`, which shares the same output).

* docs: document the OpenTelemetry export and add a New Relic guide (#14557)

Langflow has exported OTLP traces, metrics and logs for a while and none of it
was documented. Nothing in docs/ mentioned OTEL_EXPORTER_OTLP_ENDPOINT, so the
feature was discoverable only by reading the source. The existing Instana page
covers the Traceloop LLM-tracing route, which is a different thing: that one
carries prompts and completions on purpose, this one deliberately does not.

Two pages, because the vendor guides all need the same base and Instana is next.

The OpenTelemetry page covers what actually gets exported, with the attribute
set and the real protocol values read off the code rather than the plan, and
what does not: prompts, completions, exception messages, database bound
parameters. It names the two settings that widen that boundary, since an
operator should not discover LANGFLOW_OTEL_LOG_BODIES from a support ticket.
It also documents span volume and the LANGFLOW_OTEL_DB_SPANS trade, with the
measured numbers, so nobody meets that on an invoice.

The New Relic page carries the vendor specifics: the ingest license key (and
why a user API key is the wrong one), gzip for the 1 MB cap, and delta
temporality, verified by checking the exporter's preferred temporality with and
without the variable rather than assuming the SDK honours it.

Its verification section is the part worth keeping. A 2xx from New Relic is not
evidence, because it acks and then discards invalid data during async
validation, so the page gives the NRQL to confirm the data is really there,
including the NrIntegrationError check. Both traps we actually hit are written
down: the "required metrics are missing" banner is the curated view matching
semantic conventions rather than an ingest failure, and an open-ended time
window measures ingest lag and reads as 30% data loss.

Cross-links between the two new pages are relative rather than absolute, because
absolute slugs resolve against the released docs version where these pages do
not exist yet, and the build rejects them.

* feat: add OrcaRouter bundle component (#14549)

* feat: add OrcaRouter bundle component

* fix(ci): assert the base wheel's real console script name (#14571)

The "Base Distribution Wheel" job verified that the base-only environment
exposes a `langflow` console script and no `langflow-base` one. That is
inverted: `langflow-base` declares `langflow-base = langflow.langflow_launcher:main`,
while the `langflow` script belongs to the root `langflow` distribution --
which the same step explicitly forbids from that environment. The assertion
could never pass, and the follow-on boot step invoked `bin/langflow`, which
does not exist there either.

These expectations were carried over from the langflow-core wheel this job
used to test (#14352) and were never re-pointed at langflow-base. Swap both
script assertions, give them failure messages so a future break is not a bare
AssertionError, and boot the server via `bin/langflow-base`, matching
`docker/build_and_push_base.Dockerfile`.

* fix(ci): assert the base console script the 1.12 line actually ships (main) (#14586)

fix(ci): assert the base console script the 1.12 line actually ships

Ports #14584 to main. main inherited the stale `langflow-base` assertion from
#14571 when the back-merge (#14581) paired it with release-1.12.0's post-#14339
pyproject, where the base wheel declares `langflow`:

    # src/backend/base/pyproject.toml
    [project.scripts]
    langflow = "langflow.langflow_launcher:main"

Both branches have to carry this. The nightly tag push only succeeds while main
and the release branch have identical .github/workflows content -- GitHub
screens App-token pushes for workflow changes and GITHUB_TOKEN cannot carry
`workflows`, so any drift re-breaks create-nightly-tag.

Taken as release-1.12.0's copy of the file verbatim rather than re-applying the
edit, so the two branches are byte-identical by construction.

* fix(tests): measure the flow persistence barrier from the reload, not before it (#14587)

Windows Playwright shards 30/70 and 31/70 were the only failing jobs in nightly
run 31867911970; all 70 Linux shards passed, including the Linux shards running
the same spec. Five of bulk-delete-sessions.spec.ts's fourteen tests failed with

    Flow <uuid> did not finish model refresh and autosave persistence within 30000ms

reloadAndWaitForFlowPersistence created its deadline setTimeout before calling
page.reload(), so the 30s budget had to cover the page load as well as the
model refresh and autosave it is actually there to observe. Playwright serves
the editor from a Vite dev server (`npm start`), so a reload replays ~3.5k
unbundled module requests. Measured from the blob-report traces on Windows:

    trace      page.reload()   GET /flows/{id}   POST custom_component/update
    e3fe6e22        19.0s           t+27.6s          t+29.7s (1.06s)
    1a49b9dd        21.5s           t+28.9s          t+47.5s (10.8s)
    f6f421a4        34.9s              --                 --

The third reload outlasts the whole budget on its own, so that run could never
pass. Arm the deadline after the reload resolves and raise it to TIMEOUTS.long;
the worst observed post-reload cost was ~37s, and the test timeout is 5min
while these tests run 65-95s.

The barrier reaches 38 call sites across 30 spec files, so this was a latent
flake for every Windows spec that configures the loopback provider, not just
the two shards that happened to pair two playground chat builds on one runner.

* fix(ci): grant the label job pull-requests write (#14590)

Every "Label PR" run has failed since #14540 -- 67 successes and no failures
before it, 16 failures after (the successes since are runs where the job's `if:`
skips it, e.g. merge_group and bot PRs):

    POST /repos/langflow-ai/langflow/issues/14588/labels
    403 Resource not accessible by integration

#14540 added a `permissions:` block to this workflow. Before that there was
none, so it inherited the repository default, which includes pull-requests
write. Labelling a *pull request* needs that scope: the `issues` permission only
covers real issues even though the REST path is `/issues/{n}/labels`. GitHub
says so in the response itself:

    x-accepted-github-permissions: issues=write; pull_requests=write

Also unblocks Namchee/conventional-pr in the same workflow, which cannot post
its report under a read-only pull-requests scope.

* perf(tests): seed the loopback provider into starter templates instead of reloading (#14589)

`configureLoopbackOpenAI` patched the persisted flow behind the running editor
and then reloaded the page so the editor would pick the change up. Playwright
serves the app from a Vite dev server, so that reload replays ~3.5k unbundled
module requests: 19-35s on Windows CI, and it happens once per test across 38
call sites.

Nothing forces the configuration to arrive out of band. `useAddFlow` posts the
starter template the browser fetched from `/api/v1/flows/basic_examples/`, so
serving that catalog already pointed at the loopback fixture makes the flow
*born* configured — the editor and the database never diverge and there is
nothing to reload for.

`seedLoopbackProvider(page)` installs that route and must run before the first
navigation, since React Query caches the catalog for the session.
`configureLoopbackOpenAI` then takes a fast path when the flow it reads is
already configured, and keeps the patch-and-reload path otherwise, so a spec
that does not seed (or builds its flow from a blank canvas) is unaffected. The
fallback warns rather than staying silent, so the optimization cannot rot
unnoticed across the seeded specs.

The one thing that can still write these nodes without a reload is the model
refresh `useApplyFlowToCanvas` fires on mount, so the fast path waits for it.
Refreshes carry no flow in their URL — `buildRefreshPayload` stamps
`_frontend_node_flow_id` onto the template — so `modelRefreshFlowId` attributes
them, and the tracker is armed before navigation to avoid a retroactive wait.

The shared mutation and predicates move into `loopback-provider-policy.mjs`
alongside the existing `flow-editor-persistence-policy.mjs`, pure and unit
tested, so the route seeder and the patch path cannot drift apart.

Not rolled out to specs that build from a blank canvas (`decisionFlow`,
`similarity`, `Youtube Analysis`) — seeding the template catalog does nothing
for them. Deliberately opt-in rather than folded into `openStarterProject`:
`live/llm-provider-smoke.spec.ts` uses that helper and must reach a real
provider, which is exactly the failure mode #14540 fixed for the live config.

Measured locally on macOS, bulk-delete-sessions.spec.ts (8 tests, 2 workers):
2.8m before, 1.6m after, all passing both ways. macOS reloads are far cheaper
than the 19-35s measured on Windows, so the CI saving should be larger.

* fix(tests): finish the public build before closing the popup; widen the messages loading-state wait (#14595)

Nightly 31907290063 (main @ b40b405e) failed exactly two Playwright shards.

Windows 24/70 - messages.a11y "scans the named loading state": the expect
after `page.goto("/settings/messages")` used the default 5s. The trace shows
goto returning at `load`, then auto_login (1.6-3.0s) -> whoami -> config ->
the lazy settings route; the messages query mounted 7.0s / 7.5s after goto,
1.4s / 1.7s after the expect gave up. The aria snapshot at failure was the
app-level "Loading..." page, not SessionView's status. Use TIMEOUTS.standard,
which the identical held-response loading scan in knowledge-bases.a11y
already uses.

Linux 41/70 - publish-flow: the spec sent a message in the shareable
playground popup and closed it 30ms later, while the public build was still
in flight. Aborting that request mid-write made the backend terminate its
aiosqlite connections under cancellation; the trace + backend log show a
~60s window where every SQLite writer stalled (the un-publish PATCH never
answered, the retry's auto_login hung 34s+, the sibling worker's build took
71s instead of 0.66s) while reads kept answering in ms. Wait for the build
to finish (Stop visible -> hidden via the shared sendPlaygroundMessage
helper) before closing the popup, which also proves the published playground
completes a run rather than merely starting one.

Verified locally against the full Playwright stack: both tests pass.

* [autofix.ci] apply automated fixes

* fix(orcarouter): register the bundle in the sidebar and release contract

Three registration points every other lfx-bundles provider carries were
missing:

- SIDEBAR_BUNDLES had no orcarouter entry. The sidebar Bundles section is
  built from that list only, so the category fell through to the main
  components group as "Orcarouter" with a generic folder glyph, and the
  OrcaRouter icon added by this PR was never rendered.
- scripts/ci/release_inventory_contract.json omitted the bundle, which
  breaks test_contract_tracks_every_long_tail_bundle and the release
  inventory gate that compares a built image's bundle set against the
  contract. ci-scripts-test.yml is path-filtered to scripts/ci/**, so this
  PR's own CI never ran that assertion.
- The two new icon files were committed with CRLF line endings, failing
  biome check. autofix.ci tried to fix them but its cherry-pick raced with
  the uv.lock autofix commit and aborted.

Also move the lazyIconImports entry into alphabetical order.

* ci: run backend tests for provider bundle changes (#14605)

The `python` path filter never learned about `src/bundles/**` after the
bundle metapackage split. Since #13614 gated `test-backend` on
`path-filter.outputs.python == 'true'`, a PR that only touches a provider
bundle reports `python=false` and skips the entire backend suite -
including the bundle's own tests under `src/bundles/*/tests/`.

Add `src/bundles/**` to the `python` filter so bundle-only PRs run the
backend suite (and `test-templates`, which shares the same output).

* fix(orcarouter): add the migration target and bump the lfx-bundles version

Two more registration points the new bundle needs:

- test_migration_table_completeness asserts every component class under
  lfx_bundles is reachable as an ext:<bundle>:<Class>@ target, so saved
  flows resolving by class name can be upgraded. Add the bare_class_name
  entry for OrcaRouterComponent. Only that one form is added: the two
  import_path entries and the @official-pre-a legacy_slot that ported
  bundles carry describe a legacy location this bundle never had.
- bundle_release_plan flagged 'releasable source changed but version
  remains 1.1.12'. Bump lfx-bundles to 1.1.13 and the dependency floor
  in the root pyproject.

uv.lock carries the version bump as a one-line edit rather than a full
`uv lock` regen, which would have reverted autofix.ci's marker
normalization in 6b26dae with 610 lines of churn.

---------

Co-authored-by: Marc-oss-hub <315200685+Marc-oss-hub@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com>
Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
Co-authored-by: 李政达 <li18903778339@gmail.com>
Co-authored-by: 李政达 <1242427577@qq.com>
Co-authored-by: Saad Mirza <saadmirza009@gmail.com>
Co-authored-by: Saad ur Rehman <saad.urrehman@cleura.com>
Co-authored-by: olayinkaadelakun <olayinka.adelakun@ibm.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.4em-ca.ibm.com>
Co-authored-by: Janardan Singh Kavia <janardankavia@ibm.com>
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabrielf.almeida90@gmail.com>
Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
Co-authored-by: keval shah <kevalvirat@gmail.com>
Co-authored-by: Tarcio <rodriguestarcio.adv@gmail.com>
Co-authored-by: Zhengcy05 <1825478405@qq.com>
Co-authored-by: Radhakrishnan Pachyappan <gingeekrishna@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com>
Co-authored-by: Oxygen56 <jiangth99@163.com>
Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com>

* fix: guard against logging the caught exception on the flow execution path (#14561)

* fix: guard against logging the caught exception on the flow execution path

logger.error(f"failed: {e}") is wrong twice over. The exception text reaches container
stdout and the local log file no matter how OTLP export is configured, and provider
errors routinely embed the prompt, the completion, or the API key that was rejected.
Withholding log bodies from the export does not help, because this text never needed
the export to leak. It also destroys the triage signal: error() sets no exc_info, so
the exported record carries no error.type, which is the only field an operator has left
once bodies are withheld. The call site trades the one safe field for the one unsafe one.

The logs-boundary work fixed nine of these by hand. Nothing stopped the next one.

Ruff's G004 is not sufficient, measured rather than assumed. Against those nine it
catches five and misses four: it does not know structlog's aerror and aexception, which
is where those four lived. It cannot see the lazy form logger.error("boom: %s", e),
which is what G004's own fix message recommends and which still renders the exception.
It fires on logger.error(f"failed for flow {flow_id}"), which is not this problem. And
logger-objects is matched per import path, of which this repo has at least five.

So: an AST check keyed on the name bound by "except ... as NAME" rather than on
formatting style. A flow id in an f-string is not a finding; a bare aerror is. It
accepts exc_info=e and type(e).__name__, and reports f"{type(e).__name__}: {e}" because
the second slot is still the message.

Scoped to the flow execution path (base/agents, graph, components/models_and_agents),
which is where a leaked exception message carries flow content. That found 22 sites,
not the dozen estimated on the ticket, including three the f-string rule would have
missed. All 22 are fixed here, so the hook gates at zero with no baseline file to drift.

The hook uses "uv run python" rather than a bare "python", matching the majority of the
local hooks. The bare form does not resolve in a uv-managed checkout.

Pre-existing and unrelated: two catalog-policy tests in tests/unit/graph/test_graph.py
fail on this base branch with none of these changes applied. Verified by reverting every
source file and re-running.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix: close two holes in the logged-exception guard

Both found in review, both real.

The keyword allowlist was too wide. It bypassed exception, error and exc as well as
exc_info, on the assumption that an exception-shaped keyword is a traceback channel.
It is not: a structured processor renders error=str(e) into the record like any other
field, so the guard was waving through the exact leak it exists to catch. Only exc_info
carries the traceback rather than a rendered value, so only exc_info is exempt now.

Nested rebinding double-reported. ast.walk flattens the whole subtree and cannot prune,
so skipping a nested "except ... as e" node still visited its children and the same log
call was reported once per binding. The comment claimed the opposite. Replaced with an
explicit recursive descent that stops at a handler rebinding the same name, and confirmed
against a probe that previously reported line 7 twice.

Pruning is by name, so a nested handler binding a different name still descends and
cannot shield a leak from the outer binding. Both behaviours now have tests.

* fix: restamp the two embedded copies of the Prompt component, and stop over-claiming

Review found three things, all confirmed before fixing.

TranslationFlow.json and the youtube bundle's Youtube Analysis.json embed prompt.py
verbatim alongside a code_hash, and both went stale when prompt.py changed here. The
py_autofix job only walks initial_setup/starter_projects, so CI does not catch it, and
PR #14524 restamped the youtube file for the same reason, so this is a miss against
existing precedent rather than a new rule.

Both now carry the updated source and a recomputed hash. The check that matters: the
embedded code in both files hashes to e3714ffe5d15, which is what prompt.py on disk
hashes to, so the copies are byte-identical to the shipped component rather than merely
edited in the same direction.

The guard's docstring led with "the exception text reaches stdout no matter how OTLP
export is configured". That over-promises, because exc_info=e -- the fix this check tells
you to write -- also renders the full traceback to stdout through format_exc_info and
ConsoleRenderer. The real boundary is the export: _OTEL_LOG_SKIP_KEYS drops exc_info and
derives error.type and error.chain instead. Reworded so nobody reads this as "stdout is
now clean".

Added a known-gap note: only the call's own arguments are inspected, so binding the text
first (msg = f"...{e}"; logger.error(msg)) passes. Confirmed with a probe. Catching it
needs dataflow within the handler, which is a much larger check; the direct form is what
the fixed call sites looked like.

* chore: bump lfx-bundles for the restamped youtube starter project

The embedded Prompt component in the youtube bundle's Youtube Analysis.json was restamped
in the previous commit, which counts as a releasable source change. The bundle release
plan gate failed because the version stayed put.

1.1.13 -> 1.1.14, generated with scripts/ci/bundle_release_plan.py update, which the gate
names in its own error message.

Run it against a current base ref. Against a stale one it walks every bundle and bumps all
of them; from the rebased branch it touches only lfx-bundles, which is the only bundle
whose source moved.

---------

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

* test: fix playground send race on Windows CI (#14611)

* fix(mcp): scope tool calls and surface failures on the execution plane (#14522)

* fix(mcp): scope tool calls to project and exposure

* fix(mcp): report excluded flows in tools/list

* [autofix.ci] apply automated fixes

* feat(mcp): swap flow credentials for variable refs

* fix(mcp): close credential scrub bypasses

* fix(mcp): keep scrub correct across a lock retry

* fix(mcp): let a lock error retry, not lose the key

A contended write inside the scrub was swallowed and treated as a verdict
on that variable: the literal was restored and the flow saved the secret
in plaintext with a 200. Re-raise database lock errors so the caller's
retry re-runs the whole operation, and cover it with a PATCH that forces
real contention — the test only passes if the credential survives the
rollback and the flow ends up referencing a variable that exists.

* fix(mcp): resolve target URL from db globals only

request_variables on a run carry the caller's X-Langflow-Global-Var-*
headers. Feeding them to the URL resolver let whoever calls a flow choose
where it connects, and the resolved credential headers travel to that
destination — SSRF validation rejects internal targets, not an arbitrary
external one.

Split the provenance: update_tools takes url_variables, resolved only
from the database-backed globals, while headers keep resolving from the
request set, which is a documented feature. The component loads the DB
set whenever the config references variables and passes it separately;
the v2 server check already read its variables from the table.

Verified live: a server whose URL is {{HOST}}/... resolves and lists its
tool from the DB variable, and the same request carrying
X-Langflow-Global-Var-HOST: http://attacker.invalid still reaches the
legitimate target.

* fix(mcp): apply rotations and resolve env safely

Three findings from the PR review, all in the credential scrub.

Rotating a key through the node was a silent no-op. The variable name
does not depend on the value, an existing variable and an existing
mcp_server row were both left alone, and the row wins at runtime — so
every edit after the first was dropped and the old credential kept being
used. A literal arriving in a flow is the user asking for a change, so it
now updates both; only the secret-bearing maps of the stored config are
replaced, leaving the URL, mode and args the user maintains there.

env was never resolved, so a stdio server was handed the MCP_ reference
as its key while the module docstring promised portability. Resolve it
from the database-backed globals only — never from request_variables,
since env is handed to a spawned process.

Every non-empty header became a Credential global variable, so
accept: application/json cluttered the variable list. Skip an allowlist
of headers that are never secrets. Deliberately not a heuristic for
'looks secret': guessing which values are sensitive fails open into a
leak, while these specific names cannot be one.

Verified live: a rotated key reaches the stored row, accept stays
literal while the key beside it becomes a reference, and the exported
flow still runs under lfx serve — rejected with HTTP 401 on a wrong
credential, past the MCP node on the right one.

* [autofix.ci] apply automated fixes

* fix(mcp): support LANGFLOW_MCP_BASE_URL override for multi-pod deployments

- Add _get_project_base_url() helper that returns the configured mcp_base_url
  verbatim when set, bypassing host/port derivation and WSL rewriting
- Fix type narrowing in get_project_streamable_http_url and get_project_sse_url:
  guard on  so port is narrowed to int before
  reaching get_url_by_os(host: str, port: int, ...)
- Expand test coverage: add SSE fallback test and whitespace-only mcp_base_url
  edge case that were previously untested
- Update mcp_base_url docstring to document the multi-pod bind-vs-advertise
  requirement

(cherry picked from commit cd2632d73c6689d8c930e509527b9e0460abc8b7)

* fix(mcp): keep composer upstream on the local URL

LANGFLOW_MCP_BASE_URL names where clients connect. MCP Composer is a
subprocess of this process and registers the project endpoint as an
upstream member server it dials itself, so the cherry-picked override
sent pod-local traffic out through the ingress and back — behind a load
balancer that lands on a different pod, and behind TLS/auth termination
it is refused outright.

Split the two meanings: get_project_streamable_http_url stays the
advertised URL and keeps the override, while the composer registration
paths use new local builders that always derive from the bind host and
port.

Refs LE-2175

* [autofix.ci] apply automated fixes

* fix(mcp): stop leaking owner errors and credentials

Four findings from the second review pass, all reproduced before fixing.

A public project executes as its owning principal so the flow can read the
owner's variables, and the disclosure check compared that execution
principal to the flow owner — which is the same person for every
anonymous caller. Live against auth_type="none", an unauthenticated
tools/call returned the raw component error and its file path; it now
returns the generic failure while the owner still gets the detail.
Disclosure now keys off the principal that actually presented a
credential, defaulting to unset so a path that establishes no caller
loses the privilege instead of inheriting the owner's.

The gate that decides whether to load global variables looked at headers
and url but not env, while the scrub rewrites env values to MCP_ names
just like headers. A stdio server whose only secret lived in env handed
the subprocess the variable name in place of the credential.

Failure messages appended the target URL and the cause verbatim.
raise_for_status builds a message containing the full request URL, so a
401 arrived carrying the credential it had just rejected — the userinfo
and query stripping applied to the target we format ourselves was not
enough. Both describers now redact every URL they emit, including the
ones inside a cause they did not compose.

A variable that could not be created put the literal back into flow.data
and answered 200. That traded a leak for a working flow, but the caller
was never told the credential had landed in an unencrypted column that
travels through export, share and version history. Refuse the write
instead: a control that silently turns itself off is worse than one that
fails. Verified over HTTP that the 500 survives the routes' broad
exception handling on both the single and batch create paths.

Refs LE-2175

* fix(lfx): redact credential URLs from serve tracebacks

Found while smoke-testing lfx serve against a 401 endpoint. The message
was clean, but the response also carries the full traceback, and inside
it httpx's own line read: Client error '401 Unauthorized' for url
'http://user:hunter2@127.0.0.1:9411/mcp?api_key=supersecret'. That body
goes to the caller and the same text goes to the log aggregator.

Redacting only inside the MCP describers was too narrow, so the URL
reduction moves to lfx.utils.url_redaction and the serve error path
applies it to the traceback, the message and the log line.

Refs LE-2175

* component index fix

* test(mcp): pin who error details are disclosed to

The handler used to read ownership off the principal the flow executes
as. Establishing a caller is now part of its contract, and the harness
only set the execution principal, so the owner case read as anonymous
and the disclosure assertion failed.

Set the caller in the harness, and add the two cases the change exists
for: no caller at all (a public project, which executes as its owner for
an anonymous caller) and a caller who is somebody else. Both must get
the generic failure.

Refs LE-2175

* fix(mcp): stop a flow import rewriting a shared credential

Reported by Rafael Gil on LE-2175 and reproduced here byte for byte:
register ui_svc with Bearer UI-KEY-1, import a flow whose inline config
carries UI-KEY-2 without opening it, and the stored row comes back
holding UI-KEY-2 — after which an untouched flow A sends the imported
key on the wire.

mcp_server rows are keyed on (user, name) and shared by every flow of
that user, so rotating one on any write let a single import re-point all
of them, unrecoverably: the encrypted column is overwritten in place and
nothing in the response, the log or the manager reports it. A flow JSON
from a third party could plant a credential on a server name that
already exists in the recipient's instance.

This is the cost I named when I made rotation win in 81e3de2 and shipped
anyway. Rotation is right only where a literal really is the user typing
a key, so it is now scoped to update_flow and, within it, to servers the
stored flow already referenced. Import, batch create, upload and create
leave an existing row alone, which is what the module docstring always
claimed. Saving a freshly imported flow cannot adopt its credential
either, because the flow was not bound to that server before the write.

Also closes the second shape: a config carrying the documented
global-variable-name form replaced the stored literal with the string
"x-api-key", which the manager then rendered as a legitimate binding.

Refs LE-2175

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Janardan Singh Kavia <janardankavia@ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>

* fix(a11y): resolve dialog naming, nested-interactive, and label defects across the deploy flow (#14299)

* fix(a11y): resolve dialog naming, nested-interactive, and label defects across the deploy flow

* fix(a11y): resolve dangling aria-controls, dead i18n keys/props, stale e2e testids, and unlabeled inputs across the deploy flow

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.4em-ca.ibm.com>
Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>

* feat(VectorDBs): Removed sidecar dependency and disk links for KBs and MBs (#14413)

Production profile pre-flight checks for infra services.

Added conditional check, if richer service is selected like redis but is not reachable, bootup fails.

Removed sidecar dependency and disk links for KBs and MBs.

fix(kb): validate knowledge base name before persistence on every backend

The KB name traversal guard only ran inside the local-Chroma path resolver,
so remote backends (OpenSearch / Chroma Cloud / Postgres / Mongo / Astra) —
which resolve no local path — never validated the name. A crafted name like
`../victim_user/evil_kb` was accepted and persisted verbatim, colliding with
another user's KB namespace and escaping the KB root once reconciled onto
local Chroma. This surfaced as traversal tests failing under the production
profile, where the default backend resolves to a remote store.

Add a backend-agnostic `validate_kb_name` guard and run it in
`create_knowledge_base` before any backend is resolved, mirroring the existing
403 + warning contract. Memory Bases already sanitize their generated
`kb_name`, so no change is needed there.

test(lfx): make component dynamic-import tests robust to chroma presence

The dynamic-import tests used KnowledgeComponent as a stand-in for "a component
whose deps are missing in the engine-only lfx env", hard-asserting that its
module fails to import. That module's only import-time heavy dep is
``langchain_chroma`` (every ``langflow`` import in it is lazy), and some CI
environments now carry chroma transitively, so the imports succeed and five
tests failed with "DID NOT RAISE".

Branch each affected test on ``importlib.util.find_spec("langchain_chroma")``,
mirroring the existing ``test_type_checking_imports`` pattern: assert the wrapped
ImportError/AttributeError when the dep is absent, and a clean import when
present. Also add the new ``kb_disk_reconcile_enabled`` PathSettings field to
the settings-composition field inventory (191 -> 192).

chore(kb): regenerate component index and starter projects for MemoryBase

MemoryBaseComponent's source gained session-filter parsing
(``_session_filter_enabled``), execution-principal scoping on the MB lookup,
and backend-driven score normalization, but the generated artifacts were not
refreshed. Rebuild ``component_index.json`` from live source (only the
MemoryBase entry and the index sha256 change) and re-run the starter-projects
updater so the four templates that embed MemoryBase pick up the new code hash.

Fixes the failing "Update Component Index" and "Update Starter Projects" CI
checks.

fix(kb): reject chunk overlap larger than chunk size with a clear 422

RecursiveCharacterTextSplitter raises ValueError at construction when
chunk_overlap > chunk_size. Unguarded, that surfaced as an opaque 500 on
/preview-chunks and as a failed background run on /{kb}/ingest, with no
actionable message. Add a shared `_validate_chunk_params` guard, called up
front on both endpoints, that returns a 422 naming both values so the UI can
surface it inline. The per-field Form ge/le bounds stay; this adds the missing
cross-field invariant.

fix(frontend): disable KB create button when chunk preview fails

When the chunk-preview request errors (e.g. the backend rejects
chunk_overlap > chunk_size with a 422), the create/build button now greys out
so a configuration the server already rejected can't be submitted into a
failing ingestion. Tracked via a single ``chunkPreviewFailed`` flag on the
form hook, cleared on each new preview attempt and on reset, and folded into
the footer's existing ``submitDisabled`` condition.

test(lfx): guard knowledge-deps probe against the chroma sys.modules shim

The module-level ``find_spec("langchain_chroma")`` crashed collection of the
whole lfx suite with ``ValueError: langchain_chroma.__spec__ is None``. The
KB-backends conftest registers a bare ``types.ModuleType("langchain_chroma")``
shim into ``sys.modules`` when the real package is absent; that shim satisfies
the knowledge module's ``from langchain_chroma import Chroma`` (so it imports),
but its ``__spec__`` is None, which makes ``find_spec`` raise rather than return
a spec. Running the file alone passed (no conftest, no shim); the full
``make lfx_tests`` failed.

Probe via a helper that checks ``sys.modules`` first and guards ``find_spec``
with ``except (ImportError, ValueError)`` — the same pattern the conftest's own
``_is_missing`` uses — so a shimmed, a real, and an absent langchain_chroma all
resolve correctly. Not flakiness and nothing was removed: purely the probe
tripping over another suite's fixture.

test(lfx): guard import_utils knowledge probe against the chroma shim

Same fragility as test_dynamic_imports: test_return_value_types hard-asserted
that importing KnowledgeComponent fails because langchain_chroma is absent. In
the full suite the KB-backends conftest registers a langchain_chroma shim into
sys.modules, so the import succeeds and the test failed with "DID NOT RAISE".

Branch the assertion on the same sys.modules-first / find_spec-guarded probe:
assert the class resolves when the dep (or its shim) is present, and the import
error only when it is genuinely absent.

* feat(bundles): lfx-confluent bundle + watsonx.data components (IBM streamhouse) (#14612)

* fix(ci): assert the base wheel's real console script name (#14571)

The "Base Distribution Wheel" job verified that the base-only environment
exposes a `langflow` console script and no `langflow-base` one. That is
inverted: `langflow-base` declares `langflow-base = langflow.langflow_launcher:main`,
while the `langflow` script belongs to the root `langflow` distribution --
which the same step explicitly forbids from that environment. The assertion
could never pass, and the follow-on boot step invoked `bin/langflow`, which
does not exist there either.

These expectations were carried over from the langflow-core wheel this job
used to test (#14352) and were never re-pointed at langflow-base. Swap both
script assertions, give them failure messages so a future break is not a bare
AssertionError, and boot the server via `bin/langflow-base`, matching
`docker/build_and_push_base.Dockerfile`.

* fix(ci): assert the base console script the 1.12 line actually ships (main) (#14586)

fix(ci): assert the base console script the 1.12 line actually ships

Ports #14584 to main. main inherited the stale `langflow-base` assertion from
#14571 when the back-merge (#14581) paired it with release-1.12.0's post-#14339
pyproject, where the base wheel declares `langflow`:

    # src/backend/base/pyproject.toml
    [project.scripts]
    langflow = "langflow.langflow_launcher:main"

Both branches have to carry this. The nightly tag push only succeeds while main
and the release branch have identical .github/workflows content -- GitHub
screens App-token pushes for workflow changes and GITHUB_TOKEN cannot carry
`workflows`, so any drift re-breaks create-nightly-tag.

Taken as release-1.12.0's copy of the file verbatim rather than re-applying the
edit, so the two branches are byte-identical by construction.

* fix(tests): measure the flow persistence barrier from the reload, not before it (#14587)

Windows Playwright shards 30/70 and 31/70 were the only failing jobs in nightly
run 31867911970; all 70 Linux shards passed, including the Linux shards running
the same spec. Five of bulk-delete-sessions.spec.ts's fourteen tests failed with

    Flow <uuid> did not finish model refresh and autosave persistence within 30000ms

reloadAndWaitForFlowPersistence created its deadline setTimeout before calling
page.reload(), so the 30s budget had to cover the page load as well as the
model refresh and autosave it is actually there to observe. Playwright serves
the editor from a Vite dev server (`npm start`), so a reload replays ~3.5k
unbundled module requests. Measured from the blob-report traces on Windows:

    trace      page.reload()   GET /flows/{id}   POST custom_component/update
    e3fe6e22        19.0s           t+27.6s          t+29.7s (1.06s)
    1a49b9dd        21.5s           t+28.9s          t+47.5s (10.8s)
    f6f421a4        34.9s              --                 --

The third reload outlasts the whole budget on its own, so that run could never
pass. Arm the deadline after the reload resolves and raise it to TIMEOUTS.long;
the worst observed post-reload cost was ~37s, and the test timeout is 5min
while these tests run 65-95s.

The barrier reaches 38 call sites across 30 spec files, so this was a latent
flake for every Windows spec that configures the loopback provider, not just
the two shards that happened to pair two playground chat builds on one runner.

* fix(ci): grant the label job pull-requests write (#14590)

Every "Label PR" run has failed since #14540 -- 67 successes and no failures
before it, 16 failures after (the successes since are runs where the job's `if:`
skips it, e.g. merge_group and bot PRs):

    POST /repos/langflow-ai/langflow/issues/14588/labels
    403 Resource not accessible by integration

#14540 added a `permissions:` block to this workflow. Before that there was
none, so it inherited the repository default, which includes pull-requests
write. Labelling a *pull request* needs that scope: the `issues` permission only
covers real issues even though the REST path is `/issues/{n}/labels`. GitHub
says so in the response itself:

    x-accepted-github-permissions: issues=write; pull_requests=write

Also unblocks Namchee/conventional-pr in the same workflow, which cannot post
its report under a read-only pull-requests scope.

* perf(tests): seed the loopback provider into starter templates instead of reloading (#14589)

`configureLoopbackOpenAI` patched the persisted flow behind the running editor
and then reloaded the page so the editor would pick the change up. Playwright
serves the app from a Vite dev server, so that reload replays ~3.5k unbundled
module requests: 19-35s on Windows CI, and it happens once per test across 38
call sites.

Nothing forces the configuration to arrive out of band. `useAddFlow` posts the
starter template the browser fetched from `/api/v1/flows/basic_examples/`, so
serving that catalog already pointed at the loopback fixture makes the flow
*born* configured — the editor and the database never diverge and there is
nothing to reload for.

`seedLoopbackProvider(page)` installs that route and must run before the first
navigation, since React Query caches the catalog for the session.
`configureLoopbackOpenAI` then takes a fast path when the flow it reads is
already configured, and keeps the patch-and-reload path otherwise, so a spec
that does not seed (or builds its flow from a blank canvas) is unaffected. The
fallback warns rather than staying silent, so the optimization cannot rot
unnoticed across the seeded specs.

The one thing that can still write these nodes without a reload is the model
refresh `useApplyFlowToCanvas` fires on mount, so the fast path waits for it.
Refreshes carry no flow in their URL — `buildRefreshPayload` stamps
`_frontend_node_flow_id` onto the template — so `modelRefreshFlowId` attributes
them, and the tracker is armed before navigation to avoid a retroactive wait.

The shared mutation and predicates move into `loopback-provider-policy.mjs`
alongside the existing `flow-editor-persistence-policy.mjs`, pure and unit
tested, so the route seeder and the patch path cannot drift apart.

Not rolled out to specs that build from a blank canvas (`decisionFlow`,
`similarity`, `Youtube Analysis`) — seeding the template catalog does nothing
for them. Deliberately opt-in rather than folded into `openStarterProject`:
`live/llm-provider-smoke.spec.ts` uses that helper and must reach a real
provider, which is exactly the failure mode #14540 fixed for the live config.

Measured locally on macOS, bulk-delete-sessions.spec.ts (8 tests, 2 workers):
2.8m before, 1.6m after, all passing both ways. macOS reloads are far cheaper
than the 19-35s measured on Windows, so the CI saving should be larger.

* fix(tests): finish the public build before closing the popup; widen the messages loading-state wait (#14595)

Nightly 31907290063 (main @ b40b405e) failed exactly two Playwright shards.

Windows 24/70 - messages.a11y "scans the named loading state": the expect
after `page.goto("/settings/messages")` used the default 5s. The trace shows
goto returning at `load`, then auto_login (1.6-3.0s) -> whoami -> config ->
the lazy settings route; the messages query mounted 7.0s / 7.5s after goto,
1.4s / 1.7s after the expect gave up. The aria snapshot at failure was the
app-level "Loading..." page, not SessionView's status. Use TIMEOUTS.standard,
which the identical held-response loading scan in knowledge-bases.a11y
already uses.

Linux 41/70 - publish-flow: the spec sent a message in the shareable
playground popup and closed it 30ms later, while the public build was still
in flight. Aborting that request mid-write made the backend terminate its
aiosqlite connections under cancellation; the trace + backend log show a
~60s window where every SQLite writer stalled (the un-publish PATCH never
answered, the retry's auto_login hung 34s+, the sibling worker's build took
71s instead of 0.66s) while reads kept answering in ms. Wait for the build
to finish (Stop visible -> hidden via the shared sendPlaygroundMessage
helper) before closing the popup, which also proves the published playground
completes a run rather than merely starting one.

Verified locally against the full Playwright stack: both tests pass.

* ci: run backend tests for provider bundle changes (#14605)

The `python` path filter never learned about `src/bundles/**` after the
bundle metapackage split. Since #13614 gated `test-backend` on
`path-filter.outputs.python == 'true'`, a PR that only touches a provider
bundle reports `python=false` and skips the entire backend suite -
including the bundle's own tests under `src/bundles/*/tests/`.

Add `src/bundles/**` to the `python` filter so bundle-only PRs run the
backend suite (and `test-templates`, which shares the same output).

* feat(bundles): add lfx-confluent bundle and watsonx.data components for the IBM streamhouse

Langflow already orchestrates agents over IBM's data at rest (OpenRAG on
watsonx.data) but had no surface for data in motion. This adds first-class
components for IBM's streaming lakehouse -- IBM Confluent (Kafka, Tableflow,
Real-Time Context Engine) and IBM watsonx.data (Presto, remote MCP server) --
using only open protocols (Kafka wire protocol, Iceberg REST, MCP over
Streamable HTTP, Presto DBAPI).

New bundle `lfx-confluent` (src/bundles/confluent, sidebar "IBM Confluent"):
- ConfluentContextEngineComponent: preset MCP toolset for the Real-Time
  Context Engine (list_topics / get_metadata / query_data) for Agents.
- ConfluentKafkaProducerComponent: publish Message/Data/DataFrame rows to a
  topic; delivery report output.
- ConfluentKafkaConsumerComponent: bounded batch (limit + timeout) into a
  DataFrame; json/string/avro/json_schema (Schema Registry) values.
- ConfluentTableflowReaderComponent: read Tableflow Iceberg tables via the
  Tableflow REST catalog with pyiceberg (row filter, projection, limit,
  snapshot); list tables.
Deps: confluent-kafka[avro,json,schemaregistry], pyiceberg, pyarrow (lazy
imports; bundle loads without them). Registered in the `bundles` extra,
uv sources, and workspace members.

`lfx-ibm` 0.1.5 -> 0.2.0:
- WatsonxDataPrestoComponent: SQL on watsonx.data Presto (presto-python-client;
  ibmlhapikey/IAM API key or basic auth; CA bundle; row cap) -> DataFrame.
- WatsonxDataMCPComponent: preset MCP toolset for the watsonx.data remote MCP
  server (/api/v2/mcp/; bearer token or IBM Cloud API-key IAM exchange).

Core: new `lfx.base.mcp.preset.MCPPresetComponent` -- a base for components
that wrap a fixed remote MCP server as an Agent toolset. Reuses
`lfx.base.mcp.util.update_tools` (SSRF-checked Streamable HTTP with SSE
fallback), the `add_tool_output` + `_get_tools()` Tool-Mode pattern, a direct
"Response" output, and a live tool-dropdown refresh. Lives in lfx.base rather
than a bundle because the extension loader registers every *Component class
found in a bundle module.

Security: every tenant-supplied endpoint / bootstrap host goes through
`validate_connector_url_for_ssrf`; Confluent IDs are validated as plain
tokens before URL interpolation; secrets are SecretStrInput.

Frontend: Confluent icon, WatsonxData icon alias, "IBM Confluent" sidebar
bundle entry (+ sidebar test).

Docs: new bundles-confluent page (component reference plus zero-code recipes:
Context Engine via MCP Tools, HTTP Sink -> webhook trigger, Flink Streaming
Agent -> Langflow flow-as-tool, Tableflow -> watsonx.data), watsonx.data
sections on the IBM bundle page, sidebar + install-partial entries.

Tests: 230 bundle tests (src/bundles/confluent/tests, src/bundles/ibm/tests)
and 12 core tests (src/lfx/tests/unit/base/mcp/test_preset.py); all mocked,
no network. Live smoke tests against Confluent Cloud / watsonx.data are a
follow-up (need credentials).

Lock note: pyiceberg (>=0.10) caps cachetools <7, so uv.lock moves cachetools
7.1.6 -> 6.2.6; nothing in the workspace requires >=7 (only >=6 via
ibm-watsonx-ai).

* docs(bundles): use file-relative links between the IBM and IBM Confluent bundle pages

bundles-confluent only exists in the next docs version; an absolute
/bundles-confluent link from the versioned IBM page resolved against the
released docs and broke the docs draft build. File-relative links resolve
per version (same pattern as ../Develop/memory-bases.mdx).

* chore: trigger CI

* fix(lfx): skip teardown-less services during fork-safety teardown; address review feedback on the confluent/ibm bundles

---------

Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com>
Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
Co-authored-by: 李政达 <li18903778339@gmail.com>
Co-authored-by: 李政达 <1242427577@qq.com>
Co-authored-by: Saad Mirza <saadmirza009@gmail.com>
Co-authored-by: Saad ur Rehman <saad.urrehman@cleura.com>
Co-authored-by: olayinkaadelakun <olayinka.adelakun@ibm.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.4em-ca.ibm.com>
Co-authored-by: Janardan Singh Kavia <janardankavia@ibm.com>
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabrielf.almeida90@gmail.com>
Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
Co-authored-by: keval shah <kevalvirat@gmail.com>
Co-authored-by: Tarcio <rodriguestarcio.adv@gmail.com>
Co-authored-by: Zhengcy05 <1825478405@qq.com>
Co-authored-by: Radhakrishnan Pachyappan <gingeekrishna@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com>
Co-authored-by: Oxygen56 <jiangth99@163.com>
Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com>

* feat: add CustomModelProvidersEmptyState seam for model provider modal (#14619)

* feat: integrate CustomModelProvidersEmptyState into model provider components

* feat: add tests for CustomModelProvidersEmptyState and update related components

* fix(ci): realign release contracts with the confluent/watsonx bundle changes (#14625)

#14612 bumped the root `lfx-ibm` floor to >=0.2.0 and added `lfx-confluent`
to the `[bundles]` extra, but the checked-in release contracts still described
the previous state. `scripts/ci` only runs when `scripts/ci/**` changes, so the
guard never ran on that PR — it went red for every later PR that happened to
touch those paths instead.

Realign the contracts with what the application actually declares:

- `bundle_profiles.json` + regenerated `enterprise-hardened.lock.json` carry the
  `lfx-ibm>=0.2.0,<1.0.0` range from `pyproject.toml`
- `release_inventory_contract.json` registers `lfx-confluent` as an opt-in
  standalone extension: forbidden in `python-default`, required in `python-full`,
  and present in the full profile's `langflow.extensions` entry points
- `test_release_inventory.py` tracks it in `OPT_IN_STANDALONE_EXTENSIONS`

Also widen the workflow trigger so a bundle change trips this guard on its own
PR: the root `pyproject.toml`, per-bundle `pyproject.toml` files, and long-tail
bundle package roots are exactly the inputs these contracts describe.

* fix(ci): stop uv run from half-reverting the bundle install it runs against (#14631)

The bundle-guarded lfx step installs lfx-bundles outside the lock on purpose, then
runs the tests with `uv run` -- which re-syncs the environment to uv.lock first.
That reverts the lock-managed packages the bundle install upgraded and leaves the
injected ones untouched, so the tests execute against a combination neither
resolution ever produced.

It stayed invisible until langchain-openai 1.5.2 shipped on 2026-08-18. The bundle
install resolved openai 1.5.2 with langchain-core 1.5.6; `uv run` put core back to
the locked 1.5.1; and 1.5.2 against 1.5.1 cannot import -- it reads
GATEWAY_METADATA_RESPONSE_KEY, which core added in 1.5.6. Both resolutions were
fine on their own. Pinning away from 1.5.2 would have hidden this until the next
release that needs a newer core, so the mix is what gets fixed.

Verified against the failing job locally: with `uv run`, core lands on 1.5.1 and
test_lmstudio_get_model_blocks_metadata fails exactly as CI reported; with
`uv run --no-sync`, core stays on 1.5.6 and all 8 tests pass.

cross-bundle-test.yml does the same install but invokes .venv/bin/python directly,
so nothing re-syncs there and it needs no change.

* fix(authz): answer denials honestly and let owners work in their own projects (LE-1905) (#14621)

* fix(authz): answer denials honestly and let owners work in their own projects

Five defects from the LE-1905 RBAC post-V1 report that live in OSS.

Audit rows could not tell a permission check from a performed action
(finding 1). Opening a flow in the editor runs a share:create check, and
the guard wrote that check under the same action name the created-share
row uses, with no marker either way. Guards now tag every row they write
as an authorization decision, and the authz_share routes tag theirs as
the mutation, so "what was evaluated" and "what happened" are separable
in the API, the CSV export, and the UI.

A denial on a resource the caller can already read answered 404
(finding 8). The mask exists so a caller cannot probe UUIDs they have no
access to; it buys nothing once they have opened the resource, and it
costs them a truthful answer -- "verify the flow_id and try again" sends
someone to debug an identifier that is correct. deny_to_404_unless_readable
re-checks read on a denial: readable is a 403 naming the missing
permission, unreadable stays a 404. Applied to project edit and delete,
POST /api/v2/workflows and POST /api/v1/build/{id}/flow, matching what
the flow edit path already did.

Owner override did not cover creating a flow in a project you own
(finding 11). A create has no flow owner yet, so the destination project
is the only ownership the check can consult, and it was not passed. A
user holding a read-only role could not use the default project created
for them. The destination owner is now read from the stored folder row
after canonicalization -- never echoed from the request, which would let
a caller assert ownership of a project they do not own.

Privileged /authz routes validated the request body before authorizing
(finding 7). A caller with no role assignments received the same 422
field names and enum literals a superuser would, enough to map the
request contract of a route they cannot invoke. The superuser gate is now
a route dependency, which FastAPI solves before body validation. The
in-body call stays as the gate for anything reaching the endpoint without
dependency resolution.

Truncated project names in the sidebar were unreachable (finding 12) --
no tooltip, and the sidebar cannot be widened. With one default project
per user the list fills with rows that differ only in the truncated part.

Two enforcement tests asserted the behavior the report calls defective
and are updated: a viewer creating a flow in their own project now
succeeds (and is still refused in someone else's), and a read-only share
holder denied execute now gets the execute-permission 403 rather than a
404 for a flow they can see.

* fix(plugins): report a plugin config failure as what it is, not a route conflict

Every ValueError from a plugin's register() was logged as "rejected (route
conflict)". An Enterprise plugin whose SIEM configuration was invalid raised
one during import, so operators saw a conflict warning and went looking for a
duplicate path -- while the real symptom was that none of that plugin's routes
had registered at all. Only a genuine conflict now takes that branch, via a
dedicated RouteConflictError, and the other branch says the routes are missing.

* fix(authz): preserve service failures and make plugin registration atomic

Review follow-ups on the LE-1905 changes.

deny_to_404_unless_readable caught every HTTPException from its read
check, including a 5xx from the authorization plugin, and answered 404.
That reported an outage as a missing resource and sent the caller to
check an identifier that was fine — the same class of misdirection the
helper exists to remove. Only a 403 from the read check means "cannot
see it"; any other status is surfaced unchanged.

Plugin registration was not atomic: a plugin that mounted several routes
before one conflicted stayed half-live, reported as rejected while part
of its surface answered requests. For an authorization plugin that is
worse than registering nothing, because the app looks functional. Both
failure branches now roll the plugin's routes back and say how many were
withdrawn, which is also what makes the new log message true — it
claimed none of the routes were available while some were. Rollback is
kept out of the wrapper handed to plugins, since a plugin able to roll
back to an arbitrary snapshot could delete Langflow's own routes. Only
routes are undone; a plugin that installed a lifespan hook or dependency
override before failing keeps it, and the docstring says so.

Tests: a read check that raises 503 preserves the status; partial mounts
are rolled back and their reservations released so a later plugin can
claim the abandoned path; a neighbouring plugin's failure leaves a
successful one intact; the ordering matrix now covers role PATCH, team
PATCH and team-member POST, with a guard that fails if a new
superuser-gated body route is added without being listed; and the
sidebar project-name tooltip has frontend cover (two of its three cases
fail without the title attribute).

* fix(authz): let execute-only callers run from the Playground, and stop auditing capability probes

Round 2 of LE-1905. The canvas always posts its own nodes and edges, so the
owner-only graph-override gate denied every non-owner Playground run and
reframed it to 404 FLOW_NOT_FOUND. The built-in Viewer and Editor both hold
flow:execute and could run the same flow through the API, which is why this
read as "works via API, 404 via UI".

Overriding the stored graph is an edit expressed at run time, so it is now
gated on flow:write: a caller who holds write can already persist that graph
and run it. An execute-only caller has the override dropped and runs the
stored definition -- what flow:execute means -- instead of being told the flow
does not exist. The security property is unchanged: caller-supplied graph data
still never runs for someone who cannot edit the flow.

Also adds capability_probe(), which evaluates a permission for a UI capability
answer without writing an authorization-decision row. The share-capability
probe fires once per rendered resource, so it wrote a share:create row for
every project in the sidebar, with no resource and no share behind it.

* feat(authz): classify every audit row and let a reader filter on the class

Alice's round-2 finding 1 notes that audit:read and rbac_policy:startup_reconcile
rows carry no event class, so a reader filtering on it silently loses them. Adds
the two missing classes -- access for something a user did that changed nothing,
system for something the server did with no user behind it -- so every row can be
classified.

GET /api/v1/authz/audit gains event and exclude_event so "show me what people
did" is a server-side filter with an honest total and honest pagination rather
than a client-side sieve over a page. An untagged row can never satisfy an
include and is never dropped by an exclude, so rows written before
classification existed stay visible.

* test(authz): follow the override gate into the contract it is recorded in

Round 2 moved the run-time graph override from ownership to flow:write, but
left two artifacts describing the behavior it replaced.

The v1 build route test still pinned the 404 that the fix removed, so it failed
as DID NOT RAISE. It now asserts the property that actually matters -- the
caller-supplied graph never reaches the build worker -- and a companion pins
the other half: a non-owner holding write keeps the override.

The execution-principal matrix named a v2 test that the same commit renamed,
which is exactly the drift the checker exists to catch. Both build entrypoints
also still declared tweaks as owner_only; they are owner_or_writer now, so the
vocabulary gains that value. v1_run keeps owner_only -- its tweaks gate is
untouched.

* fix(sharing): move the project Share control into the three-dot menu

Knowledge Bases, Files and Deployments all render Share as an item in their
row menu. The projects sidebar was the one surface that rendered it as a
second control beside the menu trigger, and unlike that trigger it had no
hover gating -- so it was the only permanently visible control in the list,
on every row. The sidebar is the narrowest surface in the app, which is where
the extra control costs the most.

The menu was a Select driven by a value, which is a form control standing in
for a command menu: it cannot host the shared Share item, and it announces
itself as a combobox to a screen reader. It is a DropdownMenu now, matching
the other three surfaces, so Share is simply another item. The rename,
download and delete entries keep their test ids and route through the same
handler as before.

Reserving room for two controls widened the row to pr-16 when the icon was
added. One control needs pr-8, and the 2rem goes back to the project name --
which is truncated in the sidebar and cannot be widened.

* fix(a11y): distinct tab titles per route, html lang that follows the locale, no nested main (#14629)

* fix(a11y): distinct tab titles per route, html lang that follows the locale, no nested main

Only the shared playground set a document title, so every other route showed
the generic "Langflow" and was indistinguishable in tabs, history and AT
window lists. index.html hard-codes lang="en" and nothing synced it to the
active locale, so the six translated locales were announced with English
pronunciation rules. Two modals rendered their own <main> on top of the
page's, nesting a second main landmark.

Adds a useDocumentTitle hook wired into every route (reusing the string each
page already renders as its heading), syncs documentElement.lang on i18n init
and languageChanged, and turns the two modal <main> elements into <div>.

* fix(a11y): title the home page after the active tab, not the route

Flows and Deployments share /flows and only differ by the selected header
tab, and the header force-selects Flows on /components when MCP is on. Keying
the title on the route prop let the tab title disagree with the visible page;
keying it on the live flowType state cannot.

* fix(a11y): wire field labels to their controls across remaining widgets (#14461)

* fix(a11y): label and keyboard-nav fixes for canvas fields and picker popovers

* fix(a11y): wire field labels to their controls across remaining widgets

* [autofix.ci] apply automated fixes

* fix(a11y): wire accessible names for tier-1 form fields and dialog inputs

* [autofix.ci] apply automated fixes

* docs(tests): tighten a11y-mock-helpers comments

* docs(tests): tighten a11y-mock-helpers comments

* test(a11y): cover mcp aria-expanded and file-management trigger branch

* improve biome

* fix(a11y): sync real focus to cmdk's virtual selection in output-type dropdown;

* fix(a11y): forward tableTitle into TableComponent's tableLabel so the canvas table grid announces its real name instead of Data table

* remove comment

* fix output testcases

* fix(a11y): add accessible field labels to Tools, InputFile, and MCP components

* [autofix.ci] apply automated fixes

* test(a11y): add axe coverage for CustomEdges, HandleRenderComponent, and NodeInputField (#14487)

* test(a11y): add axe coverage for CustomEdges, HandleRenderComponent, and NodeInputField

* test(a11y): add axe coverage for NodeInputField and drop OutputComponent's unused selected/types/idx props

* test(a11y): add axe coverage for NodeInputField and drop OutputComponent's unused selected/types/idx props

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>

* fix(a11y): announce the MCP server value and name the key/value pair inputs

Two defects QA found on this PR.

The MCP server trigger is a plain button, not a combobox: a screen reader
announces its name and nothing else. Labelling it with the field alone
meant aria-labelledby won the name computation and the selected server was
discarded, so users heard "MCP Server" with no value. Compose the field
label and the visible value span into the name instead, and drop the
aria-label that used to carry the value on its own.

The key/value pair field labelled only its key input, leaving the value
input announced as a bare "edit text" and indistinguishable from the key
beside it. Name it from the field label plus a translated qualifier, so
the pair reads as "<field>" and "<field> value".

* fix(a11y): name every key/value row, not just the first

Naming only the first row left every later row announced as a bare
"Type a value..., edit text" — the same symptom QA reported, one row down,
and an axe `label` violation on both inputs of each additional row.

Every row is named now. Rows past the first carry their position, so they
stay distinguishable from row 1 and from each other: "Headers",
"Headers value", "Headers row 2", "Headers value row 2".

This reverses the first-row-stands-in-for-the-field rule this component
borrowed from inputListComponent. That rule leaves real controls unnamed,
which is a Level 1 failure; inputListComponent still has it and needs the
same treatment separately.

---------

Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>
Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com>

* fix: Handle duplicate name in flows batch create with 409 response (#14634)

fix(api): return clean 409 on duplicate name in flows batch create

POST /flows/batch/ builds rows directly instead of routing through
_new_flow, so a UNIQUE(user_id, name)/(user_id, endpoint_name) collision
reaches session.flush() unhandled. Left un-rolled-back on SQLite, the
failed INSERT pins the write lock and the next writer busy-waits
busy_timeout (30s) before its own "database is locked"; the raw error
also leaks the SQL statement and bound parameters.

Roll back on IntegrityError to release the lock immediately and map the
violation to a 409 via _handle_unique_constraint_error (matching
upsert_flow). Add a regression test asserting 409, no leak, and a usable
session afterward.

Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>

* fix(ci): keep skipped package releases green and stop sending target_commitish (release-1.12.0) (#14650)

fix(ci): keep skipped package releases green and stop sending target_commitish

The four determine-*-version jobs ended their "already on PyPI" branch with
`exit 1`, so a release with nothing new to publish reported red even though
every consumer gates on `outputs.skipped` rather than on the job result. Emit
the version, mark the job skipped, and exit clean.

Create Release passed `commit:` (target_commitish) unconditionally. GitHub
ignores that field once the tag exists, but sending it still runs the create
endpoint's workflow-permission check against the resolved commit, and an
Actions token is never granted the Workflows permission. The 1.11.4 release
failed five attempts with "403 Resource not accessible by integration" while
holding contents: write. Resolve the sha only while the tag is still missing,
and spell out the release job's permissions.

---------

Co-authored-by: Adam Dalloul <adam_dalloul@icloud.com>
Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com>
Co-authored-by: Varshith <106383116+Varshith-Kali@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabrielf.almeida90@gmail.com>
Co-authored-by: Marc-oss-hub <sjh668899@outlook.com>
Co-authored-by: Marc-oss-hub <315200685+Marc-oss-hub@users.noreply.github.com>
Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com>
Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
Co-authored-by: 李政达 <li18903778339@gmail.com>
Co-authored-by: 李政达 <1242427577@qq.com>
Co-authored-by: Saad Mirza <saadmirza009@gmail.com>
Co-authored-by: Saad ur Rehman <saad.urrehman@cleura.com>
Co-authored-by: olayinkaadelakun <olayinka.adelakun@ibm.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@Olayinkas-MacBook-Pro.local>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.war.can.ibm.com>
Co-authored-by: Viktor Avelino <viktor.avelino@gmail.com>
Co-authored-by: Olayinka Adelakun <olayinkaadelakun@mac.4em-ca.ibm.com>
Co-authored-by: Janardan Singh Kavia <janardankavia@ibm.com>
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
Co-authored-by: keval shah <kevalvirat@gmail.com>
Co-authored-by: Tarcio <rodriguestarcio.adv@gmail.com>
Co-authored-by: Zhengcy05 <1825478405@qq.com>
Co-authored-by: Radhakrishnan Pachyappan <gingeekrishna@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com>
Co-authored-by: Oxygen56 <jiangth99@163.com>
Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com>
E
Eric Hare committed
c3bfdb7d03deb3ce43c1f81505f545e9aaffac43
Parent: 7a975c5
Committed by GitHub <noreply@github.com> on 8/19/2026, 2:12:18 AM