Skip to content

docs: external authentication, RBAC, SSO - #13866

Merged
erichare merged 9 commits into
release-1.11.0from
docs-rbac-and-sso
Jul 9, 2026
Merged

docs: external authentication, RBAC, SSO#13866
erichare merged 9 commits into
release-1.11.0from
docs-rbac-and-sso

Conversation

@mendonk

@mendonk mendonk commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator
  • RBAC and SSO documentation
  • Some refactoring of docs now that more options are available (add overview, combine JWT page with builtin Langflow page)
  • Add keycloak example

Some additional changes for 1.11:

  • More information on rate limiting
  • Grant Langflow permission to manage Kubernetes Secrets

Summary by CodeRabbit

  • Documentation
    • Added new guidance for authentication and authorization, including built-in auth, external auth, RBAC, JWT signing, login limits, signup settings, and security-related environment variables.
    • Added deployment notes for multi-worker rate limits and Kubernetes-based storage for global variables.
    • Expanded documentation with setup examples and API references for authorization and external authentication.
  • Chores
    • Reorganized the docs sidebar and updated a redirect so the authentication docs are easier to find.

@mendonk mendonk self-assigned this Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a035a46-240c-4fe8-8940-89b4360aa283

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR adds and reorganizes documentation for authentication, authorization, external authentication, and JWT signing, updates sidebar and redirect navigation, adds a Kubernetes Secrets section for global variables, and documents rate-limit counter storage in multi-worker deployments.

Changes

Authentication documentation and navigation

Layer / File(s) Summary
Navigation and overview entrypoints
docs/docs/Develop/authentication-overview.mdx, docs/sidebars.js, docs/docusaurus.config.js
Adds a new authentication overview page, groups authentication docs under a sidebar category, and redirects /jwt-authentication to /api-keys-and-authentication.
Built-in auth and token signing
docs/docs/Develop/api-keys-and-authentication.mdx, docs/docs/Deployment/deployment-multi-worker.mdx
Expands built-in authentication docs with signup, SSRF, rate limiting, JWT signing, token expiration, and multi-worker rate-limit counter storage guidance.
External authentication flow
docs/docs/Develop/external-authentication.mdx
Adds the external authentication page with Keycloak setup, environment variables, JIT claim mapping, access ceilings, identity resolution, and deployment examples.
Authorization reference and enablement
docs/docs/Develop/authorization.mdx
Adds the authorization page with RBAC behavior, environment variables, system roles, API reference, and enablement steps.

Global variables storage

Layer / File(s) Summary
Kubernetes Secret storage
docs/docs/Develop/configuration-global-variables.mdx
Adds Kubernetes Secret storage for global variables with RBAC setup, Deployment wiring, and verification steps.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • langflow-ai/langflow#13386: Updates the same multi-worker deployment doc with Redis-backed queue configuration, which overlaps with the new rate-limit counter storage note.

Suggested labels

documentation

Suggested reviewers

  • erichare
  • phact
🚥 Pre-merge checks | ✅ 9
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main documentation changes around external authentication, RBAC, and SSO.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed PR only changes docs/redirect/sidebar files; no code paths or test files were added or updated, so test coverage isn’t applicable.
Test Quality And Coverage ✅ Passed This PR is docs-only; no implementation or test files were added/changed, so the test-quality check is not applicable.
Test File Naming And Structure ✅ Passed Changed test files use the expected names/directories, and sampled backend/frontend tests have clear, descriptive cases with proper fixtures/setup.
Excessive Mock Usage Warning ✅ Passed PR changes only docs/config/sidebar pages; no test files were added or modified, so the mock-usage warning is not applicable.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs-rbac-and-sso

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jun 26, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jun 26, 2026
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/docs/Develop/api-keys-and-authentication.mdx (1)

758-760: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the auto-authentication claim from the startup step.

This walkthrough sets LANGFLOW_AUTO_LOGIN=False, and the earlier section already says users must sign in when auto-login is off. Saying server startup "automatically authenticates you" conflicts with the next /login steps and makes the flow misleading.

Suggested fix
-Starting Langflow with a `.env` file automatically authenticates you as the superuser set in `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`.
-If you don't explicitly set these variables, the default values are `langflow` and `langflow` for system auto-login.
+Starting Langflow with a `.env` file applies the configured superuser bootstrap credentials.
+When `LANGFLOW_AUTO_LOGIN=False`, you still sign in at `/login` with those credentials.
+If you don't explicitly set these variables, the default values are `langflow` and `langflow`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/api-keys-and-authentication.mdx` around lines 758 - 760,
Remove the auto-authentication claim from the startup guidance in the API
keys/authentication docs. In the affected paragraph, adjust the Langflow startup
wording so it no longer says that launching with a .env file automatically
authenticates as the superuser; instead, align it with the
LANGFLOW_AUTO_LOGIN=False flow and the later /login steps. Keep the mention of
LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD only as
credentials/configuration context, not as an automatic login behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/docs/Develop/authentication-overview.mdx`:
- Around line 12-20: The chooser list in authentication-overview is losing its
intended hierarchy because the descriptive paragraphs after each bullet are
flush-left, so they render as standalone paragraphs instead of nested bullet
content. Reformat the text under each bullet in the authentication overview
section so the descriptions stay indented beneath the corresponding items,
keeping the list scannable in MDX. While updating that section, also correct the
wording in the second bullet from “identify provider” to “identity provider.”

In `@docs/docs/Develop/authorization.mdx`:
- Around line 179-184: The “See also” section in the authorization docs still
links to the old JWT page, so update the JWT reference to point to the merged
section under api-keys-and-authentication instead of the legacy
/jwt-authentication URL. Use the existing “See also” list in the
authorization.mdx content to locate the link and make sure any internal docs
links now target the current JWT signing section so they resolve directly
without relying on redirects.

In `@docs/docs/Develop/configuration-global-variables.mdx`:
- Around line 172-173: The Secret naming guidance is inconsistent between the
prose and the example, so update the documentation in the global variables flow
to use the same Secret name convention everywhere. Make the wording in the
section that describes Kubernetes secrets align with the name produced by
encode_user_id(user_id) and the example Secret name used in the steps, so
readers can clearly identify the actual Secret to inspect or create.
- Around line 224-235: The Kubernetes Deployment example is structurally invalid
because `serviceAccountName` and `containers` are shown directly under `spec`
instead of under `spec.template.spec`. Update the YAML snippet in the
configuration docs so the `serviceAccountName` and `LANGFLOW_VARIABLE_STORE`
container environment block are nested inside the Pod spec path used by a
Deployment, and make sure the example is valid and ready to apply as written.
- Around line 254-265: The Secret verification step in the
configuration-global-variables docs is exposing full Secret `.data` values,
which conflicts with the warning and can leak credentials. Update the
verification example to check only the stored keys, using the same Secret name
pattern and context around the UUID lookup, so readers can confirm `credential_`
and generic variable names without printing values. Keep the surrounding
guidance in sync with the warning and the existing explanation of key prefixes.

In `@docs/docs/Develop/external-authentication.mdx`:
- Around line 168-170: The external-authentication guidance is telling readers
to use jwt.io with a live access token, which is unsafe. Update the relevant
wording in the documentation to avoid recommending third-party token paste;
instead, point readers to a local JWT decoder or explicitly instruct them to
inspect only redacted tokens, while keeping the existing `aud`, `iss`, and `exp`
validation guidance in the same section.
- Around line 88-97: The external-auth example is using
langflowai/langflow-nightly:latest without explanation, so update the
walkthrough to either use the stable Langflow image/tag in the documented
current release or add a clear note in the external authentication section
stating that this example requires nightly. Make the change in the docker run
example that follows the external auth settings so readers are not switched to
nightly implicitly.

---

Outside diff comments:
In `@docs/docs/Develop/api-keys-and-authentication.mdx`:
- Around line 758-760: Remove the auto-authentication claim from the startup
guidance in the API keys/authentication docs. In the affected paragraph, adjust
the Langflow startup wording so it no longer says that launching with a .env
file automatically authenticates as the superuser; instead, align it with the
LANGFLOW_AUTO_LOGIN=False flow and the later /login steps. Keep the mention of
LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD only as
credentials/configuration context, not as an automatic login behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 239650c0-0a96-407c-a65a-6fab6aa71a15

📥 Commits

Reviewing files that changed from the base of the PR and between c1c2d59 and 61a64eb.

📒 Files selected for processing (9)
  • docs/docs/Deployment/deployment-multi-worker.mdx
  • docs/docs/Develop/api-keys-and-authentication.mdx
  • docs/docs/Develop/authentication-overview.mdx
  • docs/docs/Develop/authorization.mdx
  • docs/docs/Develop/configuration-global-variables.mdx
  • docs/docs/Develop/external-authentication.mdx
  • docs/docs/Develop/jwt-authentication.mdx
  • docs/docusaurus.config.js
  • docs/sidebars.js
💤 Files with no reviewable changes (1)
  • docs/docs/Develop/jwt-authentication.mdx

Comment on lines +12 to +20
* To secure a Langflow server with user accounts and API keys using Langflow's built-in authentication, see [API keys and authentication](./api-keys-and-authentication).
Built-in authentication is always available and is the default setting. Users log in with a username and password, and Langflow issues a short-lived JWT session token and validates Langflow API keys against its own database.

* To connect Langflow to your company's SSO, OIDC, or identify provider, see [SSO and external authentication](./external-authentication).
External authentication lets an upstream identity provider, OIDC proxy, or corporate SSO gateway handle login. Langflow accepts the token the proxy forwards, validates it against the identity provider's JWKS endpoint, and provisions a local user automatically.

* To configure RBAC on your Langflow server, see [Authorization](./authorization).
After a user is authenticated by any of the authentication paths, the authorization layer decides what the user can do.
RBAC enforcement requires a registered authorization plugin.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Indent the path descriptions under each bullet.

Lines 13, 16, and 19 are flush-left, so MDX renders them as standalone paragraphs instead of part of the chooser list. That breaks the scan pattern here, and Line 15 still says identify provider.

Suggested fix
-* To secure a Langflow server with user accounts and API keys using Langflow's built-in authentication, see [API keys and authentication](./api-keys-and-authentication).
-Built-in authentication is always available and is the default setting. Users log in with a username and password, and Langflow issues a short-lived JWT session token and validates Langflow API keys against its own database.
+* To secure a Langflow server with user accounts and API keys using Langflow's built-in authentication, see [API keys and authentication](./api-keys-and-authentication).
+  Built-in authentication is always available and is the default setting. Users log in with a username and password, and Langflow issues a short-lived JWT session token and validates Langflow API keys against its own database.
 
-* To connect Langflow to your company's SSO, OIDC, or identify provider, see [SSO and external authentication](./external-authentication).
-External authentication lets an upstream identity provider, OIDC proxy, or corporate SSO gateway handle login. Langflow accepts the token the proxy forwards, validates it against the identity provider's JWKS endpoint, and provisions a local user automatically.
+* To connect Langflow to your company's SSO, OIDC, or identity provider, see [SSO and external authentication](./external-authentication).
+  External authentication lets an upstream identity provider, OIDC proxy, or corporate SSO gateway handle login. Langflow accepts the token the proxy forwards, validates it against the identity provider's JWKS endpoint, and provisions a local user automatically.
 
-* To configure RBAC on your Langflow server, see [Authorization](./authorization).
-After a user is authenticated by any of the authentication paths, the authorization layer decides what the user can do.
-RBAC enforcement requires a registered authorization plugin.
+* To configure RBAC on your Langflow server, see [Authorization](./authorization).
+  After a user is authenticated by any of the authentication paths, the authorization layer decides what the user can do.
+  RBAC enforcement requires a registered authorization plugin.

As per coding guidelines, "keep paragraphs short and scannable."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* To secure a Langflow server with user accounts and API keys using Langflow's built-in authentication, see [API keys and authentication](./api-keys-and-authentication).
Built-in authentication is always available and is the default setting. Users log in with a username and password, and Langflow issues a short-lived JWT session token and validates Langflow API keys against its own database.
* To connect Langflow to your company's SSO, OIDC, or identify provider, see [SSO and external authentication](./external-authentication).
External authentication lets an upstream identity provider, OIDC proxy, or corporate SSO gateway handle login. Langflow accepts the token the proxy forwards, validates it against the identity provider's JWKS endpoint, and provisions a local user automatically.
* To configure RBAC on your Langflow server, see [Authorization](./authorization).
After a user is authenticated by any of the authentication paths, the authorization layer decides what the user can do.
RBAC enforcement requires a registered authorization plugin.
* To secure a Langflow server with user accounts and API keys using Langflow's built-in authentication, see [API keys and authentication](./api-keys-and-authentication).
Built-in authentication is always available and is the default setting. Users log in with a username and password, and Langflow issues a short-lived JWT session token and validates Langflow API keys against its own database.
* To connect Langflow to your company's SSO, OIDC, or identity provider, see [SSO and external authentication](./external-authentication).
External authentication lets an upstream identity provider, OIDC proxy, or corporate SSO gateway handle login. Langflow accepts the token the proxy forwards, validates it against the identity provider's JWKS endpoint, and provisions a local user automatically.
* To configure RBAC on your Langflow server, see [Authorization](./authorization).
After a user is authenticated by any of the authentication paths, the authorization layer decides what the user can do.
RBAC enforcement requires a registered authorization plugin.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/authentication-overview.mdx` around lines 12 - 20, The
chooser list in authentication-overview is losing its intended hierarchy because
the descriptive paragraphs after each bullet are flush-left, so they render as
standalone paragraphs instead of nested bullet content. Reformat the text under
each bullet in the authentication overview section so the descriptions stay
indented beneath the corresponding items, keeping the list scannable in MDX.
While updating that section, also correct the wording in the second bullet from
“identify provider” to “identity provider.”

Source: Coding guidelines

Comment on lines +179 to +184
## See also

- [API keys and authentication](/api-keys-and-authentication)
- [External authentication](./external-authentication)
- [JWT authentication](/jwt-authentication)
- [Security](/security)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Link current docs directly to the merged JWT section.

This page still points readers to /jwt-authentication, even though this PR moved JWT signing docs into api-keys-and-authentication. Using the current section URL avoids sending readers through a legacy redirect.

Suggested fix
-- [JWT authentication](/jwt-authentication)
+- [Configure JWT token signing](/api-keys-and-authentication#configure-jwt-token-signing)

As per coding guidelines, "Test internal links to ensure they are functional and reference the correct pages."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## See also
- [API keys and authentication](/api-keys-and-authentication)
- [External authentication](./external-authentication)
- [JWT authentication](/jwt-authentication)
- [Security](/security)
## See also
- [API keys and authentication](/api-keys-and-authentication)
- [External authentication](./external-authentication)
- [Configure JWT token signing](/api-keys-and-authentication#configure-jwt-token-signing)
- [Security](/security)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/authorization.mdx` around lines 179 - 184, The “See also”
section in the authorization docs still links to the old JWT page, so update the
JWT reference to point to the merged section under api-keys-and-authentication
instead of the legacy /jwt-authentication URL. Use the existing “See also” list
in the authorization.mdx content to locate the link and make sure any internal
docs links now target the current JWT signing section so they resolve directly
without relying on redirects.

Source: Coding guidelines

Comment on lines +172 to +173
When Kubernetes secrets are enabled, each user's global variables are stored in a dedicated `Opaque` Secret in the `langflow` namespace, named after the user's UUID.
Credentials are stored with a `credential_` prefix on the secret key.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the Secret name guidance consistent.

The prose says the Secret is named after the user's UUID, but the example uses uuid-YOUR_USER_ID, and the implementation derives the name via encode_user_id(user_id). That mismatch makes step 6/7 ambiguous about which Secret name readers should actually expect.

Also applies to: 252-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/configuration-global-variables.mdx` around lines 172 - 173,
The Secret naming guidance is inconsistent between the prose and the example, so
update the documentation in the global variables flow to use the same Secret
name convention everywhere. Make the wording in the section that describes
Kubernetes secrets align with the name produced by encode_user_id(user_id) and
the example Secret name used in the steps, so readers can clearly identify the
actual Secret to inspect or create.

Comment on lines +224 to +235
3. Set `serviceAccountName` and the `LANGFLOW_VARIABLE_STORE` environment variable in your Deployment:

```yaml
spec:
serviceAccountName: langflow
containers:
- name: langflow
image: langflowai/langflow:latest
env:
- name: LANGFLOW_VARIABLE_STORE
value: "kubernetes"
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Correct the YAML structure for Kubernetes Deployment

The provided YAML fragment incorrectly places serviceAccountName and containers directly under spec. In a valid Kubernetes Deployment manifest, these fields must reside under spec.template.spec (the Pod spec). Placing them at the spec level will result in an invalid resource definition or failure to apply.

Suggested fix
spec:
  template:
    spec:
      serviceAccountName: langflow
      containers:
        - name: langflow
          image: langflowai/langflow:latest
          env:
            - name: LANGFLOW_VARIABLE_STORE
              value: "kubernetes"

Ensure all code examples are valid and ready for immediate use by the reader.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/configuration-global-variables.mdx` around lines 224 - 235,
The Kubernetes Deployment example is structurally invalid because
`serviceAccountName` and `containers` are shown directly under `spec` instead of
under `spec.template.spec`. Update the YAML snippet in the configuration docs so
the `serviceAccountName` and `LANGFLOW_VARIABLE_STORE` container environment
block are nested inside the Pod spec path used by a Deployment, and make sure
the example is valid and ready to apply as written.

Comment on lines +254 to +265
7. Inspect the Secret's keys to confirm your variable was stored, replacing **YOUR_USER_ID** with the user's UUID:

```bash
kubectl get secret uuid-YOUR_USER_ID -n langflow -o jsonpath='{.data}' | python3 -m json.tool
```

Credential-type variables appear with a `credential_` prefix.
Generic-type variables use the variable name directly as the key.

:::warning
Do not read or modify Secret values directly with `kubectl`. Editing Secrets outside of Langflow can cause variables to become unreadable.
:::

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not tell readers to print Secret .data here.

Line 257 dumps every stored Secret value, just base64-encoded, which conflicts with the warning in Lines 263-264 and unnecessarily exposes credentials during verification. If the goal is only to confirm the keys, show a key-only command instead.

Suggested fix
- kubectl get secret uuid-YOUR_USER_ID -n langflow -o jsonpath='{.data}' | python3 -m json.tool
+ kubectl get secret uuid-YOUR_USER_ID -n langflow -o go-template='{{range $k, $v := .data}}{{printf "%s\n" $k}}{{end}}'

As per coding guidelines, "Verify all code examples are tested and working before committing documentation."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
7. Inspect the Secret's keys to confirm your variable was stored, replacing **YOUR_USER_ID** with the user's UUID:
```bash
kubectl get secret uuid-YOUR_USER_ID -n langflow -o jsonpath='{.data}' | python3 -m json.tool
```
Credential-type variables appear with a `credential_` prefix.
Generic-type variables use the variable name directly as the key.
:::warning
Do not read or modify Secret values directly with `kubectl`. Editing Secrets outside of Langflow can cause variables to become unreadable.
:::
7. Inspect the Secret's keys to confirm your variable was stored, replacing **YOUR_USER_ID** with the user's UUID:
🧰 Tools
🪛 LanguageTool

[style] ~264-~264: This phrase is redundant. Consider using “outside”.
Context: ...irectly with kubectl. Editing Secrets outside of Langflow can cause variables to become ...

(OUTSIDE_OF)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/configuration-global-variables.mdx` around lines 254 - 265,
The Secret verification step in the configuration-global-variables docs is
exposing full Secret `.data` values, which conflicts with the warning and can
leak credentials. Update the verification example to check only the stored keys,
using the same Secret name pattern and context around the UUID lookup, so
readers can confirm `credential_` and generic variable names without printing
values. Keep the surrounding guidance in sync with the warning and the existing
explanation of key prefixes.

Source: Coding guidelines

Comment on lines +88 to +97
docker run -d --name langflow \
--network container:keycloak \
-e LANGFLOW_AUTO_LOGIN=False \
-e LANGFLOW_EXTERNAL_AUTH_ENABLED=true \
-e LANGFLOW_EXTERNAL_AUTH_PROVIDER=keycloak \
-e LANGFLOW_EXTERNAL_AUTH_TOKEN_HEADER=Authorization \
-e LANGFLOW_EXTERNAL_AUTH_JWKS_URL=http://127.0.0.1:8080/realms/langflow/protocol/openid-connect/certs \
-e LANGFLOW_EXTERNAL_AUTH_ISSUER=http://localhost:8080/realms/langflow \
-e LANGFLOW_EXTERNAL_AUTH_AUDIENCE=langflow-app \
langflowai/langflow-nightly:latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid switching readers onto nightly without saying why.

This walkthrough is the only auth example here that pulls langflowai/langflow-nightly:latest. If external auth is documented for the current release, this should stay on the stable image/tag; otherwise the page needs an explicit note that the example requires nightly.

Suggested fix
-    langflowai/langflow-nightly:latest
+    langflowai/langflow:latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
docker run -d --name langflow \
--network container:keycloak \
-e LANGFLOW_AUTO_LOGIN=False \
-e LANGFLOW_EXTERNAL_AUTH_ENABLED=true \
-e LANGFLOW_EXTERNAL_AUTH_PROVIDER=keycloak \
-e LANGFLOW_EXTERNAL_AUTH_TOKEN_HEADER=Authorization \
-e LANGFLOW_EXTERNAL_AUTH_JWKS_URL=http://127.0.0.1:8080/realms/langflow/protocol/openid-connect/certs \
-e LANGFLOW_EXTERNAL_AUTH_ISSUER=http://localhost:8080/realms/langflow \
-e LANGFLOW_EXTERNAL_AUTH_AUDIENCE=langflow-app \
langflowai/langflow-nightly:latest
docker run -d --name langflow \
--network container:keycloak \
-e LANGFLOW_AUTO_LOGIN=False \
-e LANGFLOW_EXTERNAL_AUTH_ENABLED=true \
-e LANGFLOW_EXTERNAL_AUTH_PROVIDER=keycloak \
-e LANGFLOW_EXTERNAL_AUTH_TOKEN_HEADER=Authorization \
-e LANGFLOW_EXTERNAL_AUTH_JWKS_URL=http://127.0.0.1:8080/realms/langflow/protocol/openid-connect/certs \
-e LANGFLOW_EXTERNAL_AUTH_ISSUER=http://localhost:8080/realms/langflow \
-e LANGFLOW_EXTERNAL_AUTH_AUDIENCE=langflow-app \
langflowai/langflow:latest
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/external-authentication.mdx` around lines 88 - 97, The
external-auth example is using langflowai/langflow-nightly:latest without
explanation, so update the walkthrough to either use the stable Langflow
image/tag in the documented current release or add a clear note in the external
authentication section stating that this example requires nightly. Make the
change in the docker run example that follows the external auth settings so
readers are not switched to nightly implicitly.

Comment on lines +168 to +170
If authentication fails, mismatched `aud` or `iss` values are the most common causes.
Optionally, decode the token at [jwt.io](https://jwt.io/) and confirm `iss`, `aud`, and `exp` match the audience mapper.
The `aud` array should include `langflow-app` from the audience mapper.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't tell readers to paste a live access token into a third-party site.

The token in this flow is usable against Langflow. Recommend a local decoder, or explicitly tell readers to inspect only redacted tokens instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/docs/Develop/external-authentication.mdx` around lines 168 - 170, The
external-authentication guidance is telling readers to use jwt.io with a live
access token, which is unsafe. Update the relevant wording in the documentation
to avoid recommending third-party token paste; instead, point readers to a local
JWT decoder or explicitly instruct them to inspect only redacted tokens, while
keeping the existing `aud`, `iss`, and `exp` validation guidance in the same
section.

@mendonk mendonk added the DO NOT MERGE Don't Merge this PR label Jun 26, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jun 30, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added documentation Improvements or additions to documentation and removed documentation Improvements or additions to documentation labels Jun 30, 2026
@github-actions

This comment has been minimized.

@erichare
erichare force-pushed the release-1.11.0 branch 2 times, most recently from 3924fa1 to 951ea94 Compare July 1, 2026 18:27
@erichare
erichare deleted the branch release-1.11.0 July 1, 2026 18:34
@erichare erichare closed this Jul 1, 2026
@erichare erichare reopened this Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

@mendonk
mendonk requested a review from HimavarshaVS July 9, 2026 13:42
@mendonk mendonk removed the DO NOT MERGE Don't Merge this PR label Jul 9, 2026
@github-actions github-actions Bot added the lgtm This PR has been approved by a maintainer label Jul 9, 2026
@mendonk
mendonk added this pull request to the merge queue Jul 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 9, 2026
@erichare
erichare merged commit 6ea7dc0 into release-1.11.0 Jul 9, 2026
36 checks passed
@erichare
erichare deleted the docs-rbac-and-sso branch July 9, 2026 16:11
jordanrfrazier pushed a commit that referenced this pull request Jul 13, 2026
* docs: add rate limiting and signup env vars

* docs: configure global vars in k8s secrets

* docs: per-process login rate limiting

* docs: initial rbac and sso content

* docs: move jwt page to auth and add redirect

* docs: add keycloak example

* docs: authorization plugin interface
erichare added a commit that referenced this pull request Jul 13, 2026
…rve (#13818)

* feat(lfx): per-request-isolated flow execution for lfx serve

Run `lfx serve --workers N` flows under per-request process isolation so
cross-request os.environ credential leakage is structurally impossible,
while sharing the warm library + flow graphs across workers via COW.

- gunicorn --preload + UvicornWorker + max_requests=1: the master builds and
  warms the FlowRegistry once and forks workers (copy-on-write); each worker is
  recycled after one request.
- serve_preloaded_app.py: import-time master entrypoint (build_registry_from_env
  + gc.freeze) inherited by forks.
- serve_gunicorn.py: LFXGunicornApp process manager.
- _EXECUTE_GUARD asyncio.Semaphore(1) + guarded_execute around
  execute_graph_with_capture (run + stream), closing the async concurrent-overlap
  window that max_requests=1 alone leaves open.
- commands.py: _launch_workers launches gunicorn on Unix; refuses --workers>1 on
  Windows (gunicorn is Unix-only).
- credentials.py: gate the post-DB-miss SECRET_KEY-rotation env fallback behind
  is_env_fallback_disabled() so no_env_fallback is honored (A3).
- FlowRegistry.get logs store reconstruction so per-request rebuild cost of
  uploaded flows is observable.
- gunicorn>=22.0 added as a platform-gated (non-Windows) lfx dependency.

Tests: process-model (preload/warm/guard/fork-safety), synchronous run contract,
no_env_fallback credential regression; existing multi-worker serve tests migrated
to the gunicorn launcher. 173 serve+credentials tests green.

* Add pre-fork hook

* feat(lfx): add opt-in --reset-environ and --sync-workers to lfx serve

Two opt-in flags for per-request isolation in multi-worker lfx serve, both defaulting to the existing committed behavior:

- --reset-environ: snapshot/restore os.environ around each flow run so a flow's env mutations (or request-scoped credentials) cannot leak into the next request served by the same warm worker (gated by LFX_SERVE_RESET_ENVIRON; read per request in guarded_execute).

- --sync-workers: serve via gunicorn's blocking sync worker behind an a2wsgi ASGI->WSGI bridge so the kernel routes each request to an idle worker (one whole request per worker at a time) instead of queueing behind an in-flight request on a busy async worker. The bridge is built lazily post-fork; refused on Windows.

Also fix LFXGunicornApp.load() to honor the app import string (it hardcoded the ASGI app, which fed the WSGI sync worker the wrong callable), and declare a2wsgi as a Unix-only dependency alongside gunicorn.

* feat(lfx): add --timeout flag for lfx serve worker timeout

Expose gunicorn's worker timeout (previously hardcoded at 120s) as --timeout, on both serve_command and the CLI wrapper. A worker that doesn't complete a request within --timeout seconds is killed and restarted.

Matters most under --sync-workers: a blocking sync worker cannot heartbeat mid-request, so long-running flows need a higher timeout. Default stays 120s (unchanged behavior); no effect on the Windows uvicorn fallback.

* fix(lfx): register VariableService in serve so request-scoped global_vars resolve

lfx serve never registered a VariableService, so get_variable_service() returned
None in the worker. Every credential path that resolves through it
(get_api_key_for_provider, get_all_variables_for_provider, model_utils, KB
connectors) therefore never consulted the request scope and fell back to
os.environ -- which --no-env-fallback then blocks. As a result, request-scoped
global_vars could not supply a model/agent provider API key unless the flow's
api_key field was explicitly wired to load the variable.

Register the minimal in-memory VariableService in create_multi_serve_app
(idempotent; a real DB-backed service is left untouched), making all of those
paths request-scope-aware at once. Guard the MCP component's DB-only
get_all_decrypted_variables call with hasattr so the minimal service is skipped
cleanly instead of raising into its broad except.

Verified end-to-end: Basic Prompting and Simple Agent starter flows execute
successfully under --no-env-fallback with the OpenAI key supplied only via
global_vars, while a request without global_vars still fails (no env leakage).

* refactor(lfx): rework lfx serve worker/concurrency flags

- Remove --limit-concurrency; --use-sync-workers covers bounded concurrency
- Rename --sync-workers to --use-sync-workers/--use-async-workers (default async)
- Default --max-requests to periodic recycling (~1000, 10% jitter) for memory
  hygiene; 0 disables, explicit N overrides
- Make Windows/a2wsgi flag refusals loud (typer.echo, not verbose_print)
- Declare uvicorn[standard] so standalone lfx installs get uvloop/httptools
- DRY: shared serve --help strings (_serve_help) + env context manager in
  _launch_workers
- Document --use-sync-workers / --reset-environ as the per-request isolation
  mechanisms (async --max-requests is worker hygiene, not isolation)

* [autofix.ci] apply automated fixes

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

* fix(lfx): warn on --timeout with single worker; drop dead limit-concurrency env

- Make --timeout an int|None sentinel (matching --max-requests) so the
  single-worker path can warn that it is ignored instead of silently
  honoring it; None resolves to DEFAULT_TIMEOUT (120) for gunicorn.
- Remove the unused _SERVE_LIMIT_CONCURRENCY_ENV constant.
- Dedup the FakeGunicornApp test stubs behind _make_fake_gunicorn_app.

* update dep docs

* simplify cmmments

* skip gunicorn threads in ghost finder

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

* fix(lfx): address review comments on serve worker flags

- normalize reset_environ/sync_workers OptionInfo sentinels for direct
  serve_command() callers (truthy sentinels silently enabled the flags)
- _exported_env now restores overwritten env vars instead of deleting them
- clarify MAX_REQUESTS help: strict isolation needs --reset-environ, or
  --use-sync-workers WITH --max-requests 1 (sync worker alone is not isolation)
- correct Windows fallback comment in pyproject
- assert LFX_SERVE_RESET_ENVIRON cleanup; reset preloaded module state in test
- add synchronous error-contract test for /run

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

* fix(lfx): close reset-environ auth race and warn on unisolated multi-worker

- guarded_execute restores os.environ by diff instead of clear()+update(): clear()
  empties key-by-key and could race a threadpool-run verify_api_key into a 401
- snapshot the API key at app startup (app.state) so auth never reads live os.environ
  per request; lazy-cache fallback preserves post-creation config (tests)
- warn when --workers > 1 has no per-request isolation (no --reset-environ and not
  --use-sync-workers with --max-requests 1)
- wrap the gunicorn import with a friendly error, matching the a2wsgi check
- xfail(strict=False) for the timing-dependent async-recycle leak test
- --workers help: gunicorn on Unix, uvicorn on Windows
- tests: diff-restore add/change/delete + no-clear lock, cached-key auth, isolation warning

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

* fix: prevent LangfuseResourceManager deepcopy failure in Agent/Tool Calling Agent (#13966)

* fix: prevent LangfuseResourceManager deepcopy failure in Agent components

Adds __deepcopy__ to _RootRunReparentingCallbackHandler so that
deepcopy(component) calls in component_tool.py no longer trigger
LangfuseResourceManager.__new__() with no credential kwargs.

Root cause: the #13429 fix (c4d27818a) introduced
_RootRunReparentingCallbackHandler as a CallbackHandler subclass.
CallbackHandler internally holds a LangfuseResourceManager whose
__new__ now requires explicit public_key/secret_key/base_url in newer
langfuse builds. Python's deepcopy calls __new__ with no args during
copy construction, raising:
  LangfuseResourceManager.new() missing required keyword arguments

The handler carries no per-invocation mutable state, so returning self
from __deepcopy__ is safe. The same root cause and workaround are
already documented in flow_loader.py (agentic path) for issue #13429.

Adds a focused regression test asserting deepcopy(handler) is handler
and does not raise.

Fixes #13965

* fix(ruff): replace unused lambda args a/kw with _ to fix ARG005

* fix(ci): publish bundle deps before nightly main (#13982)

* fix(ci): publish bundle deps before nightly main

* fix(ci): wait for bundle pypi propagation

---------

Co-authored-by: Eric Hare <ericrhare@gmail.com>

* feat(mcp): persist MCP servers in a database table (#13976)

* feat(mcp): persist MCP servers in a database table

MCP servers were stored in a per-user JSON file guarded by an in-process lock, so concurrent edits lose updates and the list diverges across workers/replicas. Store them in a new mcp_server table (one row per server, unique on (user_id, name)); get_server_list/get_server/update_server become per-row reads/upserts and the in-process lock is removed, so MCP config is safe at any worker/replica count. Secret values (env/headers) are encrypted at rest. Existing _mcp_servers_*.json files are imported by an idempotent startup backfill and a `langflow migrate-mcp` CLI; the file is kept for rollback. REST shape and function signatures are unchanged.

Fixes #13970

* fix(mcp): clear hashed cache-key variants when a server changes

The shared tool cache is keyed {server_name}:{hash of headers+timeout} (MCPComponent._mcp_servers_cache_key), so clearing only the bare server name left servers with custom headers/timeouts serving stale config after an update. Clear the bare name and all {server_name}: variants.

* fix(mcp): re-apply create/merge rules after IntegrityError refetch

The IntegrityError fallback in update_server reused the pre-race config, so a concurrent same-name create (check_existing) could overwrite the winning row and a concurrent PATCH (merge_existing) skipped the merge. Now re-apply the rules against the refetched row: check_existing raises 'already exists', merge_existing merges, otherwise overwrite. Adds a concurrency test.

* fix(mcp): order get_server_list by created_at to preserve insertion order

get_server_list had no ORDER BY, so with several rows the server list order was undefined. The legacy file (a dict) preserved insertion order and the UI relies on it (the starter project must stay first). Order by created_at to restore stable insertion order.

* fix(mcp): re-point migration onto current base head to resolve alembic multi-head

release-1.11.0 gained migration c3e7a1b9d2f4 after this branch opened. Our migration also chained off 4f0d2c9a8b7e, producing two alembic heads so 'alembic upgrade head' failed and the DB never initialized (cascading to all e2e/integration/docker jobs). Re-point down_revision from 4f0d2c9a8b7e to c3e7a1b9d2f4 so the chain is linear with a single head.

---------

Co-authored-by: Saad ur Rehman <saad.urrehman@cleura.com>

* docs: external authentication, RBAC, SSO (#13866)

* docs: add rate limiting and signup env vars

* docs: configure global vars in k8s secrets

* docs: per-process login rate limiting

* docs: initial rbac and sso content

* docs: move jwt page to auth and add redirect

* docs: add keycloak example

* docs: authorization plugin interface

* fix(docker): give runtime user a writable npm cache so stdio MCP servers start (#13992)

* fix(docker): give runtime user a writable npm cache so stdio MCP servers start

The ubi10 runtime base sets HOME=/opt/app-root/src, and the main image runs
`npm install -g` as root, which seeds a root-owned npm cache at
/opt/app-root/src/.npm. The runtime user (uid 1000) spawns stdio MCP servers
via `npx`, which then fails with `EACCES` on ~/.npm/_cacache, so every stdio
MCP server registers but never lists any tools (toolsCount stays null in
GET /api/v2/mcp/servers?action_count=true).

Pin npm's cache to a uid-1000-owned directory via NPM_CONFIG_CACHE=/app/.npm
(immune to the base image's HOME) and hand ownership of the default HOME cache
to the runtime user as a fallback. Applied to all three runtime images
(main, with_extras, base) that ship Node so npx has a writable cache.

* fix(docker): avoid incompatible npm latest

* fix: thin single focus border on global-variable input (#13995)

fix input style on GV input

* ci: add non-blocking flaky test report job with 30-day retention (#13996)

* ci: add non-blocking flaky test report job with 30-day retention

* fix: tolerate partial blob downloads in flaky-report job

One of the ~62 shard blob artifacts failed its download after 5 retries
(transient artifact storage error), which failed the whole download step
and skipped the merge. For a metrics-only job, merging the shards that
did download beats producing nothing: mark the download step
continue-on-error and no-op the merge cleanly when no blobs landed.

* fix(a11y): improve API keys tab order (#13953)

* fix(frontend): repair flow list card a11y

* fix(frontend): clear flows page a11y debt

* test(frontend): update header count a11y names

* test(e2e): open flow cards via action button

* fix(a11y): address flow card review

* chore: update secrets baseline

* fix(a11y): improve settings table scans

* refactor(a11y): isolate table scan fixes

* fix(a11y): improve API keys tab order

* test(frontend): fix shard click targets

* fix(a11y): link API key expiry label

* fix(a11y): resolve AG Grid accessibility bugs in table component

- Fix hidden row control restoration bug by tracking original tabindex values
- Fix pagination button self-latching bug by using only class as input signal
- Add comprehensive unit tests for both fixes (10 tests, all passing)
- Export patchGridAccessibility function and AgGridAccessibilityLabels type for testing
- Improve api-keys.a11y.spec.ts to be less brittle with dynamic checks

Addresses feedback from keval718 on PR #13953

* fix(a11y): rework API keys table keyboard nav and clear IBM violations

Replace the AG Grid DOM-patching hack (MutationObserver + global keydown/
focusin listeners + role rewriting + multi-timeout retries) with a minimal,
event-driven patch applied via the grid's own events.

Keyboard/tab order:
- Tab from the last cell reaches the next control in one press (was three via
  two dead <body> stops) using AG Grid's tabToNextCell hook.
- Disabled pagination buttons are no longer tab stops, and a container-scoped
  focusin redirect keeps AG Grid from programmatically focusing them; inert/
  disabled attributes are avoided because they break the tab guards and trap
  reverse (Shift+Tab) entry into the grid (WCAG 2.1.2).
- API key name cell opens by keyboard (Enter/Space).

IBM Equal Access: 8 -> 0 violations on /settings/api-keys (and the shared
/settings/global-variables table):
- tab guards given role=button + label (element_tabbable_role_valid, 4.1.2)
- empty rowgroups demoted to role=presentation (aria_child_valid, 1.3.1)
- first body row made tabbable via roving tabindex (aria_child_tabbable)

Also: ProviderListItem and the text-cell modal trigger become real buttons,
and Shortcuts settings cells open by keyboard.

Tests: Playwright single-Tab exit + reverse-tab re-entry regression guards,
tab-order test updated to the roving-tabindex model, and Jest unit tests for
the tab-guard / rowgroup / roving-row / disabled-paging patches.

* fix(frontend): center update toast buttons vertically (#14004)

* docs: lfx user docs and bundle extension changes (#13775)

* docs: add LFX user documentation section

Creates a new Lfx/ folder in the docs with three pages covering
the LFX executor from a user perspective:

- lfx-overview.mdx: what LFX is, when to use it, and a full command
  reference table linking to the DevOps SDK docs
- lfx-install.mdx: install from PyPI, with bundles, from source, or via
  uvx — plus bundle install guidance for standalone lfx users
- lfx-run.mdx: lfx serve and lfx run with full option tables,
  stdin/inline JSON, Python script usage, and component category
  allowlist/blocklist controls

Also adds an LFX sidebar category in the "Develop & Deploy" section
and a tip in flow-devops-sdk.mdx noting that standalone lfx installs
do not include bundle package components.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: move Flow DevOps SDK from API Reference to LFX sidebar

* docs: add terminal sidebar icon for LFX section

* docs: move Flow DevOps SDK page into Lfx folder

* docs: move extensions to lfx section

* docs: clean up lfx overview

* docs: extension manifest update and release notes

* docs: revert release notes

* docs: add back nextplaid bundle to release notes

* docs: move mcp and compatibility, add prewarm

* docs: fix relative link

* docs: partials for lfx-bundles

* docs: clarify bundle differences

* docs: remove lfx prewarm

* docs: fix broken lfx user docs link

* docs: lfx bundle installation

* docs: update graduated bundles

* docs: torch opt-in changes

* docs: fix partial link

* docs: bundles some small fixes

* docs: allowlist and blocklist for lfx

* docs: separate run and serve pages

* docs: final extensions check

* docs: lfx MCP page instructions

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a11y): make /assets/files accessible (#13987)

* fix(a11y): make /assets/files accessible (LE-1744)

Bring the Files page to WCAG 2.1 A/AA, verified with IBM Equal Access
plus keyboard/focus tests.

- Name the icon-only actions column header, hidden visually (4.1.2)
- Open the row actions menu by keyboard from the grid cell (2.1.1)
- Label the upload/bulk-delete buttons so they are named on mobile (4.1.2)
- Restore a keyboard-only :focus-visible ring on borderless grid cells (2.4.7)
- Merge the bulk-delete dialog trigger via asChild to remove the
  duplicate tab stop (2.4.3)
- Move focus into the cell editor when renaming (2.4.3)
- Make the upload "try again" retry a real button (2.1.1)

Add a dedicated Playwright a11y suite (files.a11y.spec.ts) covering the
full state matrix and keyboard/focus behaviour, move the IBM baseline
folder under tests/a11y, and baseline the known Radix menu-portal
landmark item.

* feat(a11y): add frontend accessibility check skill documentation

* fix(a11y): return focus to upload button after file picker cancel (LE-1744)

Keyboard-activating Upload Files then dismissing the native picker with
Esc dropped focus to <body> (WCAG 2.4.3). Only blur on mouse clicks (to
keep the #13178 tooltip fix); restore focus to the trigger on keyboard
activation. Adds two behavioural a11y tests.

* feat(playground): content blocks frontend renderer for v2 workflows (#13391)

* feat: native v2 workflows endpoint with pluggable stream protocols

Rebased onto release-1.10.0. The base independently rebuilt the v2
workflows backend (RBAC, body globals, share-aware fetch); keep our
forward design and conform its auth to that work:

1. Auth: keep get_current_user_for_workflow (session-or-API-key authN
   that does not hold a DB connection during the inline run, avoiding
   the SQLite lock contention api_key_security would cause) and enforce
   the base's RBAC on top: ensure_flow_permission(EXECUTE) before run,
   (READ) before status reconstruct, with widen_for_shares fetch.
2. Port the base's request-body globals onto the v2 WorkflowRunRequest.
   The X-LANGFLOW-GLOBAL-VAR-* headers stay supported (the Responses API
   passes globals that way); body globals win on conflict. Converters
   echo the effective globals via effective_globals.
3. Public endpoint keeps the v1 build_public_tmp posture
   (access_type==PUBLIC, run-as-owner); RBAC applies to the
   authenticated endpoint only.
4. Preserve the base's post-build KB-cache invalidation in the AG-UI
   build path.

The endpoint, AG-UI bridge, pluggable stream adapters, public endpoint,
and re-attach are unchanged.

* feat(api): add output_text and session_id to v2 workflow response

The synchronous /api/v2/workflows response keyed every result under its
component id, so reading the answer meant knowing an id you can't predict.
Surface two additive fields:

- output_text: the flow's single text answer (ChatOutput/TextOutput). None
  when the flow has zero or multiple text outputs, so callers read outputs
  rather than the shortcut guessing which channel is the answer.
- session_id: echoes the resolved session so chat/memory callers can
  continue the same thread (v1 /run returned this; v2 had dropped it).

outputs is unchanged, so this is non-breaking.

* test(api/v2): cover output_text and session_id on the v2 workflow response

Pin the sync-response shortcuts on the v2 workflows endpoint:
- output_text surfaces the lone ChatOutput/TextOutput text and stays None for
  non-output message nodes, data-only flows, and multi-text flows
- session_id echoes the resolved session; the error response exposes neither
- each outputs entry exposes only {type, status, content, metadata}, with the
  component id carried by the dict key

Also drop the component_id kwarg the converter passed to ComponentOutput, which
has no such field and silently dropped it.

* feat(api/v2): structured output with resolution reason on v2 response

Replace the flat output_text shortcut with an `output` object carrying the
resolved text answer plus a `reason` that explains why it resolved that way
(single/multiple/none/non_string/failed), so a null answer is always
diagnosable instead of silently None. `reason` follows the LLM-domain
finish_reason/stop_reason convention, distinct from the lifecycle status.

Also add `display_name` to each ComponentOutput (the stable component id
stays the dict key) and a computed `has_errors` flag derived from errors.

* feat(api/v2): add request-side output selection (output_ids)

Let a sync caller name the output(s) they want via output_ids so
output.text resolves deterministically (reason=single) on multi-output
flows instead of going null. Selection is steer-only: it picks the
answer among the named outputs without filtering the outputs map.

Invalid ids are rejected with 422 before the flow runs (and before any
job row is created), so a typo costs no compute. Resolution considers
selected outputs that actually fired, so branching flows resolve to
whichever candidate ran.

* feat(api/v2): emit per-output events on the langflow stream

Give v2-workflows sync and the langflow stream protocol one parser. The
stream now emits a normalized "output" event per terminal output carrying
an OutputEvent (the ComponentOutput shape sync returns in outputs[id], plus
component_id). A shared build_component_output() backs both the sync
converter and the adapter, and the build loop ships authoritative vertex
metadata as an additive output_meta key on end_vertex (existing consumers
read build_data and ignore it).

This is access-pattern parity (one parser, same fields, same terminal set),
not byte-identical content: the stream reuses the v1 build path whose
display serialization differs from sync's run_graph output.

* feat: make content_blocks the source of truth for Message content

Migrate Message.text from a Pydantic field to a @computed_field over
content_blocks, and unify ContentBlock into the discriminated ContentType
union so a Message's payload is one uniform shape.

Schema changes:
- Add 7 new content types (Image, Audio, Video, File, Reasoning, Usage,
  Citation) with validators for media sources, non-negative tokens, and
  ordered citation indices
- Promote 'contents: list[ContentType]' to BaseContent so any node can
  nest (multimodal tool outputs, multi-step reasoning, grouped errors)
- Fold ContentBlock into BaseContent and into the ContentType union with
  tag 'group'; content_blocks is now 'list[ContentType]' everywhere
- Fix Data.__setattr__ to route through property descriptors via MRO walk

Setter / serialization:
- text setter appends a single TextContent at the end of content_blocks,
  preserving non-text blocks in chronological order (tool calls first,
  final text last)
- model_post_init preserves explicit None in data['text'] when no
  TextContent exists in content_blocks, so callers can still distinguish
  'text was never set' from 'text was set to empty string'

from_lc_message:
- Handle AIMessage tool_calls and usage_metadata regardless of whether
  content is a string or a list (tool-calling agents commonly emit
  content='' alongside tool_calls)
- Tolerate explicit source=None in multimodal image payloads

MessageResponse.from_message / MessageTable.from_message:
- Accept any of (data['text'] set, text_stream pending, content_blocks
  non-empty) as 'content present', so tool-call-only and media-only
  messages persist rather than getting rejected as missing required
  fields

Tests cover all new content types, the unified ContentType union, the
text/content_blocks contract, the setter's chronological append, and the
required-fields gate.

* feat: stable id on content blocks + plumb LangChain tool_call_id

Adds an optional 'id: str | None' field to BaseContent for stable
identity across re-emissions of the same logical block. Producers
that have a natural id (LangChain tool_call_id, external API id, a
UUID stamped before the first emission) set it; consumers use it for
dedup and cross-frame correlation. Without an id, consumers fall back
to position-derived dedup, which assumes content_blocks is append-only
within a message lifetime.

Plumbs LangChain's 'tool_call_id' through 'Message.from_lc_message'
into 'ToolContent.id'. The same logical tool call across start, args
streaming, and result lifecycle now carries the same id, so a
re-fired add_message dedups to one ToolContent instead of producing
duplicates.

Tests cover id default/round-trip/inheritance across every concrete
content type, plus tool_call_id stability across repeated conversion,
multiple tool calls each keeping their own id, and tool_calls
alongside string content.

* fix(schema): MessageResponse parses microsecond timestamps and ContentBlock partial updates preserve unset fields

Two schema regressions surfaced in QA across the content-blocks chain:

1. MessageResponse.timestamp was typed as a bare datetime, but
   Message.timestamp default is a string with microsecond precision and
   a UTC timezone label ('%Y-%m-%d %H:%M:%S.%f %Z') that Pydantic's
   default datetime parser rejects. Any freshly built Message routed
   through MessageResponse.from_message raised ValidationError. Reuse
   the shared str_to_timestamp_validator so MessageResponse accepts
   every format Message itself recognises.

2. ContentBlock.__init__ marked every field as model_fields_set, not
   just the discriminator. The override defeated exclude_unset for the
   group content type: a patch like ContentBlock(title='new') dumped
   every defaulted field and, when merged onto an existing block by
   aupdate_messages, overwrote fields the caller never touched. Mark
   only 'type' (the discriminator) so partial updates carry the variant
   tag without clobbering the rest.

Adds regression tests in test_message_content_blocks.py: from_message
round-trips Message.timestamp without crashing, and ContentBlock
exclude_unset stays narrow to the explicit fields plus the
discriminator.

* [autofix.ci] apply automated fixes

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

* feat(playground): content blocks frontend renderer for v2 workflows

* feat(frontend): add v2 (beta) tab to the API Access modal

Add a v1 / v2 (beta) version toggle above the language tabs. The v2 tab
emits Python, JavaScript, and cURL snippets for POST /api/v2/workflows.

The two examples are framed by outcome, not by API jargon: "Get the full
result" (the default single JSON response) and "Stream the result as it
runs", each with a one-line plain-language description. The streaming
snippets consume the default langflow protocol (switch on the event field;
handle add_message, token, and end) rather than forcing the agui protocol.
The API key is read from an env var, and a short response peek shows the
shape a caller gets back.

* feat(frontend): v2 examples read the answer from output.text

* feat(agent): interleaved text + tool_use rendering and tabbed tool-output visualizer

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* fix(api/v2): buffer parallel messages in the AG-UI translator instead of dropping them

Parallel components stream tokens for different message ids interleaved.
The translator tracked a single open message: the first foreign token
closed the open message and tombstoned its id, so every later event for
it was dropped and its remaining text never reached the client.

Tokens for a message that cannot take the wire now buffer until the open
message genuinely ends (its add_message finalizer), then flush in arrival
order; complete messages landing mid-stream buffer the same way instead
of interleaving a second START. end/error drain all buffers before the
terminal event. The wire still carries at most one open text message, so
the stream stays AG-UI-conformant.

* [autofix.ci] apply automated fixes

* fix(api/v2): gate AG-UI message finalization on non-partial state and purge removed buffers

A partial add_message re-fire (the agent path emits these at tool
start/end for a message it is still streaming) was treated as the
finalizer: it closed and tombstoned the id, so the post-tool answer
was dropped. Only a non-partial add_message finalizes now; state
defaults to complete, so payloads without properties are unchanged.

remove_message now purges a buffered message and tombstones its id,
so text the backend retracted is not flushed to the client later.

* fix(playground): scheme-guard content block URLs against unsafe schemes

Lift safeUrl into a shared chatComponents/url.ts and route the file
content block's download anchor through it, degrading unsafe-scheme
URLs to a non-clickable label. Gate the image/audio/video src through
the same helper so a data:text/html or attacker-chosen src is dropped
rather than rendered. Matches the existing citation sanitization in
SourcesStrip. Adds file-branch tests for an http URL and a
javascript: URL.

* refactor(playground): extract content-block layout + media renderers, drop favicon egress

- Extract resolveContentBlockLayout from the duplicated shape-detection
  block in bot-message and chat-message into chat-messages/utils, with a
  direct unit test (legacy group, interleaved flat, text-only, divergent
  text, edit mode).
- Split image/audio/video/file renderers out of ContentDisplay into
  MediaContentDisplay so the dispatcher stops growing per content type.
- Replace the Google favicon fetch in SourcesStrip with a local Globe
  icon so cited source domains aren't leaked to a third party.
- Trim WHAT comments to WHY, add typed test factories.

* feat: make content_blocks the source of truth for Message content

Migrate Message.text from a Pydantic field to a @computed_field over
content_blocks, and unify ContentBlock into the discriminated ContentType
union so a Message's payload is one uniform shape.

Schema changes:
- Add 7 new content types (Image, Audio, Video, File, Reasoning, Usage,
  Citation) with validators for media sources, non-negative tokens, and
  ordered citation indices
- Promote 'contents: list[ContentType]' to BaseContent so any node can
  nest (multimodal tool outputs, multi-step reasoning, grouped errors)
- Fold ContentBlock into BaseContent and into the ContentType union with
  tag 'group'; content_blocks is now 'list[ContentType]' everywhere
- Fix Data.__setattr__ to route through property descriptors via MRO walk

Setter / serialization:
- text setter appends a single TextContent at the end of content_blocks,
  preserving non-text blocks in chronological order (tool calls first,
  final text last)
- model_post_init preserves explicit None in data['text'] when no
  TextContent exists in content_blocks, so callers can still distinguish
  'text was never set' from 'text was set to empty string'

from_lc_message:
- Handle AIMessage tool_calls and usage_metadata regardless of whether
  content is a string or a list (tool-calling agents commonly emit
  content='' alongside tool_calls)
- Tolerate explicit source=None in multimodal image payloads

MessageResponse.from_message / MessageTable.from_message:
- Accept any of (data['text'] set, text_stream pending, content_blocks
  non-empty) as 'content present', so tool-call-only and media-only
  messages persist rather than getting rejected as missing required
  fields

Tests cover all new content types, the unified ContentType union, the
text/content_blocks contract, the setter's chronological append, and the
required-fields gate.

(cherry picked from commit 3b92500349420e2651a08587aa4e4292314ef8bf)

* feat: stable id on content blocks + plumb LangChain tool_call_id

Adds an optional 'id: str | None' field to BaseContent for stable
identity across re-emissions of the same logical block. Producers
that have a natural id (LangChain tool_call_id, external API id, a
UUID stamped before the first emission) set it; consumers use it for
dedup and cross-frame correlation. Without an id, consumers fall back
to position-derived dedup, which assumes content_blocks is append-only
within a message lifetime.

Plumbs LangChain's 'tool_call_id' through 'Message.from_lc_message'
into 'ToolContent.id'. The same logical tool call across start, args
streaming, and result lifecycle now carries the same id, so a
re-fired add_message dedups to one ToolContent instead of producing
duplicates.

Tests cover id default/round-trip/inheritance across every concrete
content type, plus tool_call_id stability across repeated conversion,
multiple tool calls each keeping their own id, and tool_calls
alongside string content.

(cherry picked from commit a3e8b40811dd72eb1650b299dccb20be7cd0c8cf)

* fix(schema): MessageResponse parses microsecond timestamps and ContentBlock partial updates preserve unset fields

Two schema regressions surfaced in QA across the content-blocks chain:

1. MessageResponse.timestamp was typed as a bare datetime, but
   Message.timestamp default is a string with microsecond precision and
   a UTC timezone label ('%Y-%m-%d %H:%M:%S.%f %Z') that Pydantic's
   default datetime parser rejects. Any freshly built Message routed
   through MessageResponse.from_message raised ValidationError. Reuse
   the shared str_to_timestamp_validator so MessageResponse accepts
   every format Message itself recognises.

2. ContentBlock.__init__ marked every field as model_fields_set, not
   just the discriminator. The override defeated exclude_unset for the
   group content type: a patch like ContentBlock(title='new') dumped
   every defaulted field and, when merged onto an existing block by
   aupdate_messages, overwrote fields the caller never touched. Mark
   only 'type' (the discriminator) so partial updates carry the variant
   tag without clobbering the rest.

Adds regression tests in test_message_content_blocks.py: from_message
round-trips Message.timestamp without crashing, and ContentBlock
exclude_unset stays narrow to the explicit fields plus the
discriminator.

(cherry picked from commit 6f6639374faf52bfd7d0041a3fa3aec7a1c26a8a)

* fix(schema): address content_blocks review feedback

- sync langflow-base ContentBlock.__init__ with the lfx copy (model_fields_set
  parity) so exclude_unset no longer clobbers type; add cross-module regression test
- route MessageResponse content_blocks discriminator-first so stored flat blocks
  with contents=[] validate instead of raising
- move Message SecretStr coercion into model_post_init and drop the dead
  validate_text before-validator
- drop the no-op _fold_text_into_content_blocks validator
- log a shape-only debug line when from_lc_message drops an undecodable image
- type MessageResponse.content_blocks as list[ContentType] | None

(cherry picked from commit 6c7cec8a529c38d2f9eb7a35f94277611c8696cc)

* fix(agents): stop duplicating the final answer in content_blocks

With content_blocks as the source of truth, Message.text is a computed
field whose setter appends a trailing top-level TextContent. handle_on_chain_end
also appended the same answer into the Agent Steps group, so the final
answer rendered twice (assert 2 == 1 in test_multiple_events). The streaming
path already relies on the setter alone; make the non-streaming path match.

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

* feat(schema): project new content_blocks back to the v1 wire shape

The in-memory Message and the v2 (AG-UI) path use the new content_blocks
union (groups tagged "group", every node carries id/contents, the agent
answer is a trailing top-level TextContent). The v1 API keeps emitting the
pre-1.11.0 shape via a pure legacy_render projection that runs only at the
v1 boundaries: the v1 read/response models, the memories endpoint, the v1
build SSE stream, the webhook events SSE stream, and the /run response and
stream. The build, webhook, and /run projections recurse so the Data mirror
(data.data.content_blocks) is projected alongside the top-level copy. v2
serializes the live Message and keeps the new shape.

* [autofix.ci] apply automated fixes

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

* [autofix.ci] apply automated fixes

* fix(api): keep simple_run_flow returning RunResponse, project v1 at the HTTP boundary

simple_run_flow is a shared helper, so wrapping its return in a JSONResponse to
apply the v1 content_blocks projection broke internal callers that call
.model_dump() on the result (the streaming run_flow_generator and
get_build_results). Return the RunResponse object from the helper and apply the
projection at the non-stream HTTP boundary in _run_flow_internal instead. The
streaming path already projects the end event via _project_run_event.

* [autofix.ci] apply automated fixes

* fix(frontend): route media content URLs through safeUrl

The media content type rendered <img src={url}> with the raw url, unlike
the sibling image/audio/video/file cases that guard via safeUrl. A
javascript:/data: scheme from untrusted tool output reached the img src.
Apply the same safeUrl guard and skip rendering when it returns null.

* fix(frontend): detect untyped legacy groups in content-block layout

resolveContentBlockLayout detected groups with type === "group", missing the
legacy / v1-projected "Agent Steps" group that is persisted without a type
field (just title + contents). Those untyped groups failed hasGroup and were
miscounted as flat non-text items, wrongly enabling ordering mode so the
duplicate top-level text rendered above the tools and the bubble body was
suppressed. Use the shared isGroupedBlock predicate, matching the rest of the
render pipeline. Also document the latent tool-less-group gap in
ContentBlockDisplay (a no-tool answer projects to a text-only group; the drop
is benign today but the gate keys off toolItems, not groupedBlocks).

* fix(frontend): render a group's displayable non-tool content

ContentBlockDisplay gated entirely on a group's tool_use leaves, so a group
whose contents had no tool_use (reasoning / citation / media, …) rendered
nothing. Collect a group's displayable non-tool leaves and render them through
the same loose renderer as top-level flat leaves, while keeping text and usage
out so the legacy v1 Input/Output scaffolding stays hidden (the bubble body
already paints the answer). Adds collectGroupLooseLeaves and unit tests.

* test(frontend): deep probes for ContentBlockDisplay group rendering

Renders the real ContentBlockDisplay (real ContentDisplay / ToolCallCard /
SourcesStrip / accordion; only ESM infra is mocked) and asserts the DOM for
each content_blocks shape: a tool-less group's citation and media now render,
an untyped legacy group renders its non-tool content, a tool-bearing group
renders both the tool and its extra leaf, the legacy Input/Output text stays
hidden, text-only and usage-only groups render nothing, and the flat shape is
unchanged. Confirmed RED on the pre-fix component (4/8 fail) and GREEN after.

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

* chore: re-trigger CI after [skip ci] bake commit

* fix: regenerate component_index.json after agent.py content_blocks change

The merged index still carried AgentComponent's pre-#13390 code, so the
runtime hash allow-list (built from the index) didn't match the live
agent.py. That failed the custom-component admin-only known-template
carve-out (403 instead of 200) and drifted Update Component Index.
Rebuilt via 'make build_component_index'; sha256 now 070077b2.

* [autofix.ci] apply automated fixes

---------

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

* feat(api): durable background execution service (store + default backend) (#13507)

* feat: native v2 workflows endpoint with pluggable stream protocols

Rebased onto release-1.10.0. The base independently rebuilt the v2
workflows backend (RBAC, body globals, share-aware fetch); keep our
forward design and conform its auth to that work:

1. Auth: keep get_current_user_for_workflow (session-or-API-key authN
   that does not hold a DB connection during the inline run, avoiding
   the SQLite lock contention api_key_security would cause) and enforce
   the base's RBAC on top: ensure_flow_permission(EXECUTE) before run,
   (READ) before status reconstruct, with widen_for_shares fetch.
2. Port the base's request-body globals onto the v2 WorkflowRunRequest.
   The X-LANGFLOW-GLOBAL-VAR-* headers stay supported (the Responses API
   passes globals that way); body globals win on conflict. Converters
   echo the effective globals via effective_globals.
3. Public endpoint keeps the v1 build_public_tmp posture
   (access_type==PUBLIC, run-as-owner); RBAC applies to the
   authenticated endpoint only.
4. Preserve the base's post-build KB-cache invalidation in the AG-UI
   build path.

The endpoint, AG-UI bridge, pluggable stream adapters, public endpoint,
and re-attach are unchanged.

* feat(api): add output_text and session_id to v2 workflow response

The synchronous /api/v2/workflows response keyed every result under its
component id, so reading the answer meant knowing an id you can't predict.
Surface two additive fields:

- output_text: the flow's single text answer (ChatOutput/TextOutput). None
  when the flow has zero or multiple text outputs, so callers read outputs
  rather than the shortcut guessing which channel is the answer.
- session_id: echoes the resolved session so chat/memory callers can
  continue the same thread (v1 /run returned this; v2 had dropped it).

outputs is unchanged, so this is non-breaking.

* test(api/v2): cover output_text and session_id on the v2 workflow response

Pin the sync-response shortcuts on the v2 workflows endpoint:
- output_text surfaces the lone ChatOutput/TextOutput text and stays None for
  non-output message nodes, data-only flows, and multi-text flows
- session_id echoes the resolved session; the error response exposes neither
- each outputs entry exposes only {type, status, content, metadata}, with the
  component id carried by the dict key

Also drop the component_id kwarg the converter passed to ComponentOutput, which
has no such field and silently dropped it.

* feat(api/v2): structured output with resolution reason on v2 response

Replace the flat output_text shortcut with an `output` object carrying the
resolved text answer plus a `reason` that explains why it resolved that way
(single/multiple/none/non_string/failed), so a null answer is always
diagnosable instead of silently None. `reason` follows the LLM-domain
finish_reason/stop_reason convention, distinct from the lifecycle status.

Also add `display_name` to each ComponentOutput (the stable component id
stays the dict key) and a computed `has_errors` flag derived from errors.

* feat(api/v2): add request-side output selection (output_ids)

Let a sync caller name the output(s) they want via output_ids so
output.text resolves deterministically (reason=single) on multi-output
flows instead of going null. Selection is steer-only: it picks the
answer among the named outputs without filtering the outputs map.

Invalid ids are rejected with 422 before the flow runs (and before any
job row is created), so a typo costs no compute. Resolution considers
selected outputs that actually fired, so branching flows resolve to
whichever candidate ran.

* feat(api/v2): emit per-output events on the langflow stream

Give v2-workflows sync and the langflow stream protocol one parser. The
stream now emits a normalized "output" event per terminal output carrying
an OutputEvent (the ComponentOutput shape sync returns in outputs[id], plus
component_id). A shared build_component_output() backs both the sync
converter and the adapter, and the build loop ships authoritative vertex
metadata as an additive output_meta key on end_vertex (existing consumers
read build_data and ignore it).

This is access-pattern parity (one parser, same fields, same terminal set),
not byte-identical content: the stream reuses the v1 build path whose
display serialization differs from sync's run_graph output.

* feat(api/v2): durable background execution service (store + default backend)

Turns v2 mode:background into a durable, in-API background execution service behind a BackgroundExecutionService facade. Adds the store layer (result/error columns, job_events durable milestone log, execution_signals control, heartbeat/lease, 3 migrations), the default backend (bounded executor, runner, in-memory live bus, liveness-aware single-flight orphan sweep), the v2 endpoint rewiring, and the real-instance test harness. Needs no new infra; works on the SQLite single-process install. The redis-scaled worker backend is stacked on top in a follow-up PR.

* test(background-execution): rename hard_proof marker to real_services

The hard_proof marker name was a vibe word that said nothing about what the
tests need. Rename it to real_services everywhere: the pytest marker
registration, the *_hard_proof.py test files, the Makefile target
(real_services_tests), the -m selector in migration-validation.yml, and the
CI job. real_services says what these tests require: real Postgres + Redis +
worker subprocesses. (integration was already taken for the external-API
suite under tests/integration.)

* fix(api/v2): enforce no-code-execution gate on public workflow endpoint

The v2 public endpoint only ran validate_flow_for_current_settings and
skipped validate_public_flow_no_code_execution, which the v1
build_public_tmp path applies. A public flow containing a Python
interpreter/REPL (or the legacy Python Code Structured tool, Smart
Transform lambda) was therefore an unauthenticated server-side
code-execution primitive (report H1-3754930).

Mirror v1: import the validator and call it right after the
public-access gate. PublicFlowValidationError subclasses
CustomComponentValidationError, so the existing handler already
sanitizes it to a 400 'This flow cannot be executed.' without leaking
the blocked component class names.

Add a non-mocking test that builds a public flow with a real
PythonREPLComponent and asserts the sanitized 400 (verified RED: returns
200 without the gate).

LE-1389

* fix(api/v2): reconstruct background workflow status from job-keyed vertex builds

A completed background job's GET status 500'd with 'No vertex builds found
for job_id'. The background build path differed from the sync path twice:

1. generate_flow_events minted a fresh run_id instead of using job_id, so
   vertex builds were keyed by an id the status query never uses. Thread
   run_id through _stream_event_frames -> generate_flow_events and pass
   job_id from the background buffer so graph.run_id == job_id (the sync
   path already does graph.set_run_id(job_id)).

2. The SSE build loop (build_vertices) only persisted builds when log_builds
   was set and never passed job_id. Tie log_builds to job-tracked runs
   (run_id present) and pass job_id=graph.run_id on the persist call.
   Job-tracked runs also persist streaming terminal vertices so
   reconstruction is complete; the live build path (run_id is None) keeps
   its original behavior, so the v1 build path is unchanged.

Test: a real background run polled to completion, then GET status asserts a
reconstructed 200 (verified RED: 500 'No vertex builds found' before the
fix). Covers the non-streaming flow. v1 build path unchanged (35 build
tests pass); AG-UI suite 46 pass.

LE-1389

* fix(api/v2): merge workflow AG-UI cancellation hardening LE-1389

* fix(api/v2): signal cross-worker workflow stops LE-1389

* fix(api/v2): report unconfirmed workflow stops LE-1389

* fix(api/v2): keep background workflows out of polling watchdog LE-1389

* fix(api/v2): buffer parallel messages in the AG-UI translator instead of dropping them

Parallel components stream tokens for different message ids interleaved.
The translator tracked a single open message: the first foreign token
closed the open message and tombstoned its id, so every later event for
it was dropped and its remaining text never reached the client.

Tokens for a message that cannot take the wire now buffer until the open
message genuinely ends (its add_message finalizer), then flush in arrival
order; complete messages landing mid-stream buffer the same way instead
of interleaving a second START. end/error drain all buffers before the
terminal event. The wire still carries at most one open text message, so
the stream stays AG-UI-conformant.

* fix(api/v2): gate AG-UI message finalization on non-partial state and purge removed buffers

A partial add_message re-fire (the agent path emits these at tool
start/end for a message it is still streaming) was treated as the
finalizer: it closed and tombstoned the id, so the post-tool answer
was dropped. Only a non-partial add_message finalizes now; state
defaults to complete, so payloads without properties are unchanged.

remove_message now purges a buffered message and tombstones its id,
so text the backend retracted is not flushed to the client later.

* Fix AG-UI workflow lifecycle edges

* [autofix.ci] apply automated fixes

* fix(frontend): enable downlevelIteration for jest Set/Map iteration

ts-jest compiles with target es5; without downlevelIteration, [...set] and
for...of over a Set/Map emit ES5 that yields nothing. That silently broke the
AG-UI bridge tests: runningNodeIds spread, markRunningNodesFailed, and
restoreOriginalBuildStatuses all iterated empty. Production (Vite/SWC, modern
target) was never affected; only the ts-jest harness was. Fixes the 3 failing
jest tests on this branch with no other suite changes (4994/4994 pass).

* fix(api/v2): surface inactivated branch vertices over AG-UI

A branch component (If-Else, Conditional Router) reports its not-taken
vertices in build_data.inactivated_vertices, but the AG-UI translator only
emitted the branch node's own success/error status and dropped that list. The
canvas seeds every planned node as pending from vertices_sorted; skipped
vertices then get no build_start/end_vertex, so they stayed stuck on pending
instead of rendering as inactive (the v1 build path marked them INACTIVE).

The translator now appends an inactive STATE_DELTA op per inactivated vertex,
and the frontend bridge maps the new inactive status to BuildStatus.INACTIVE
and tears its edges down like a completed node. Fixes the If-Else regression
in general-bugs-reset-flow-run.spec.ts.

* fix(api/v2): dedupe repeated inactive node deltas in AG-UI stream

build.py keeps reporting a conditionally-excluded vertex in
inactivated_vertices on every subsequent end_vertex (the excluded set
persists until the ConditionalRouter clears it), so the translator was
putting the same inactive STATE_DELTA on the wire once per remaining
vertex. Track emitted inactive nodes and skip re-emitting; drop a node
from the set when it actually runs again (build_start/end_vertex) so a
loop re-activation can still re-emit inactive later.

* fix(api/v2): no duplicate WORKFLOW job row on durable background runs

A v2 background run created TWO JobType.WORKFLOW rows for one flow execution:
the durable row (submit()'s job_id, owned by JobRunner) plus an orphan keyed by
the run_id generate_flow_events mints, because the build pipeline's
track_job_status defaults True and the durable frame source never passed False.
The flow ran once (double bookkeeping), but every background run left a phantom
WORKFLOW row + job_events and double-fired the memory-base hook, skewing metrics.

Thread track_job_status through _stream_event_frames; pass False only from the
background frame source (the durable runner already owns the row + fires the
hook with the durable job_id). Stream/public paths keep default True. Also gate
build.py's memory-base hook fire behind track_job_status so background doesn't
double-fire. Adds a regression test (RED before fix: found 2 rows).

* test(api/v2): update stale workflow-stop tests for the durable design

These 3 tests targeted the removed queue-service stop helper
(_cancel_workflow_queue_job / get_queue_service), inherited via the
agui->bg-default merge and failing with AttributeError across the stack:

- test_stop_workflow_success: adapted to the durable stop path
  (revoke_task -> stop_job -> update_job_status(CANCELLED)).
- test_stop_workflow_allowed_for_legacy_job_with_no_user_id (IDOR): adapted to
  the durable mechanism; still asserts the ownership check does not block a
  legacy user_id=None row.
- test_stop_workflow_returns_503_when_queue_cancel_cannot_be_confirmed: dropped
  — the durable stop writes a best-effort STOP signal and always finalizes
  CANCELLED; the queue-service 'cannot confirm -> 503' path no longer exists.

test_workflow.py now passes 24/24.

* fix(api/v2): move FrameSourceFactory alias under TYPE_CHECKING

As a module-level runtime value, FrameSourceFactory = Callable[..., Any] is a
GenericAlias that passes isinstance(obj, type) but makes issubclass(obj, Service)
raise on Python 3.10/3.14. The service factory scans this module for Service
subclasses (services/factory.py:90), so the runtime alias crashed service
initialization on those interpreters with 'issubclass() arg 1 must be a class'
-> 'Could not initialize services', erroring out dozens of unrelated tests at
setup (3.13 was unaffected, which is why local runs passed).

The alias is only referenced in a lazy annotation (from __future__ import
annotations), so moving it + the Callable import under TYPE_CHECKING removes it
from the runtime namespace with no behavior change.

Verified: alias absent from runtime module namespace, factory scan finds
BackgroundExecutionService cleanly, durable service tests 26/26 pass.

* test(lfx): register background-execution Settings fields in the field-count gate

The durable background-execution work added six Settings fields
(background_max_concurrency, background_job_timeout, background_lease_ttl_s,
background_heartbeat_interval_s, background_watchdog_interval_s, test_redis_url)
without updating EXPECTED_FIELDS, so test_field_count_unchanged failed 152 != 146.
These are intentional bg-exec config; add them to the gate.

* fix(api/v2): restore "end" side-channel event in AG-UI workflow stream

The durable background-execution rewrite of workflow.py reverted
`side_channel_events` to its pre-"end" form, dropping the "end" event
from the AG-UI side-channel. That event carries `build_duration` to the
playground chat-view, and the message metadata badge only renders when
`hasDuration || hasTokens`. With build_duration gone the badge vanished,
failing the token-usage and shareable-playground "Finished In" regression
tests. Re-add "end" so the streaming playground path delivers it again.

* fix(api/v2): apply request tweaks on the streaming and background paths

The v2 workflows endpoint applied `tweaks` only on mode=sync. The stream
and background paths build the graph via the v1 build-vertex loop
(`generate_flow_events`), which never received the tweaks, so they were
silently dropped. The confusing symptom: a model passed via tweaks
surfaced as "A model selection is required", and any per-component
override was ignored on non-sync runs.

Thread `parsed.tweaks` into `generate_flow_events` and apply them to the
built graph via `vertex.update_raw_params`. We do not use the lfx
`process_tweaks_on_graph` helper because it only sets `vertex.params`,
which does not persist to runtime (the same bug
`lfx.base.tools.run_flow._process_tweaks_on_graph` works around). No-tweaks
runs are unchanged (guarded by `if tweaks`). Adds a streaming regression
test that overrides ChatInput via tweaks and asserts the value drives the run.

* fix(api/v2): return background run output from completed status

A completed background run's GET status returned a bare COMPLETED with an
empty `outputs` and a null `output`. The COMPLETED branch reconstructs
from `vertex_builds` keyed by job_id, which the durable path does not
write, so reconstruction raised ValueError and fell through to an empty
response; the result was only retrievable via a /events re-attach.

The runner now captures the terminal `output` events (the langflow
adapter's normalized ComponentOutput payloads) into `Job.result`, and the
status COMPLETED branch rebuilds the `outputs` map and resolved `output`
from them via `workflow_response_from_output_events`, matching the sync
response. agui-protocol runs emit no `output` events, so their status
stays result-less (the result remains on the /events log).

* fix(api/v2): address review findings on the v2 workflows endpoint

- recover session_id for completed background jobs from the persisted
  terminal message instead of always returning null, so GET status can
  continue the same chat/memory thread
- replay a user-cancel as a CUSTOM cancel marker + RUN_FINISHED (agui) and
  a `cancelled` terminal (langflow) instead of RUN_ERROR, so a re-attaching
  client no longer reads a deliberate stop as a failure
- cancel the evicted still-running buffer writer when the background-run
  registry is full, so it stops appending into a run no reader can find
- derive per-component status from the error artifact / valid flag instead
  of hardcoding COMPLETED, and stop the langflow adapter dropping `valid`
- throttle the unauthenticated public endpoint per IP and bound its
  input_value/session_id length
- document the sync-only scope of request-body globals
- document that live event re-attach is intentionally owner-only

* test(lfx): register public_flow_rate_limit_per_minute in settings composition

* refactor(v2 workflows): split workflow.py and address review blockers

Splits the ~1.5k-line workflow.py into focused modules and folds in the
execution-timeout and error-sanitization fixes from Cristhianzl's review of #13307.

- B1: workflow.py now holds only the four route handlers. Validation guards move
  to workflow_validation, the sync/stream run loop to workflow_execution, and the
  durable background machinery to workflow_background (layered, acyclic).
- I1: add workflow_execution_timeout (default 300) and apply a single wall-clock
  ceiling across sync, stream, background, and public via _stream_event_frames. A
  timeout becomes a sanitized terminal error and marks a background job failed.
- I3: the route error handlers no longer echo raw exception text. They return a
  generic, code-tagged message and log the full exception server-side.
- R1: remove the "commented out / future scope" comments that sat over live
  dataframe-extraction code in converters.py.
- R4: drop the worker-routing internals from the reattach 409 message.

Tests cover the timeout terminal-error path and the error-body sanitization, and
the settings field-count guard is updated for the new setting.

* refactor(lfx): extract v2 workflow contract layer into lfx.workflow

Moves the protocol-agnostic pieces of the v2 workflows API out of the langflow
backend into lfx so both the backend and `lfx serve` can share one contract.
First step toward giving lfx (the production runtime) the v2 workflows API.

- Move api/v2/adapters/, agui_translator.py, and converters.py to lfx/workflow/.
  They depend only on lfx.schema.workflow and ag_ui (already an lfx dep), so lfx
  carries the contract with zero langflow imports.
- Decouple the one langflow reference: converters typed run_response against
  langflow.api.v1.schemas.RunResponse (TYPE_CHECKING only). Replaced with a local
  RunResponseLike Protocol (outputs + session_id), the only attributes used.
- Repoint the six backend v2 workflow modules to import from lfx.workflow.
- Move the five protocol-agnostic contract tests into src/lfx/tests/unit/workflow/
  (run in the lfx-only env). test_output_event_parity and test_workflow_agui stay
  in langflow (they need langflow.api.build) with repointed imports.

Coverage unchanged: 201 contract tests pass in the lfx-only env, 191 backend v2
tests pass; 392 total, same as before the move.

* fix(background-execution): prevent worker deadlock on stop() under Python 3.10

The bounded executor's worker awaited the in-flight job task with a bare
`await task`. On Python 3.10, when stop() cancels a worker while its job
task is finishing, the awaiter's wakeup is lost and the event loop idles
forever in select(), deadlocking stop(). Await via a done-callback Event
(the same mechanism stop()'s own asyncio.gather already uses), which
delivers the wakeup reliably; task.result() preserves the cancellation
and exception semantics of the bare await.

* fix(background-execution): keep ephemeral frames on reattach, pin psycopg in real-service tests

The real-service CI job broke on `ModuleNotFoundError: No module named
'asyncpg'`. asyncpg was never declared anywhere in this repo; it arrived
transitively through `cuga`, which release-1.11.0 dropped when it moved the
root dep to `lfx-bundles[all-no-torch]` (#13886). Normalize the harness URL to
`postgresql+psycopg` instead: it is what `--extra postgresql` installs and what
DatabaseService already selects for async Postgres, so the test now exercises
the production driver rather than a stowaway.

Also fix a real reattach bug. The runner publishes ephemeral token frames
tagged with the last durable seq (they have no job_events row), while
reattach's tail skipped anything with `seq <= highest`. After replay left
`highest` at that same seq, every token delta was dropped until the next
durable milestone advanced it, so a reconnect mid-stream saw no tokens. Mark
frames durable/ephemeral and dedupe only the durable ones, which are the only
frames a replay can return.

Log instead of silently suppressing a failed stop_job signal, and drop a
comment block duplicated verbatim in InProcessExecutor.stop().

* fix(migrations): merge alembic heads (mcp_server + execution_signals)

---------

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

* docs: AGUI protocol, workflows quickstart, update examples (#13911)

* docs: curl code snippet contains placeholder

* docs: update workflows example to match new schema

* docs: include ag-ui in workflow api reference

* docs: workflows api agui streaming

* docs: workflows api quickstart and ag-ui example

* docs: fix broken build links

* docs: agui peer review

* feat(frontend): render the flow share seam in the editor Share dropdown (#14009)

The CustomFlowShareAction customization seam only rendered in the flow
list's card menu, so overlays that implement user/team sharing had no entry
point inside the flow editor. Render the same seam at the top of the
editor's Share dropdown; the OSS stub renders nothing, so the OSS menu is
unchanged. Adds an optional menuContext prop to the seam so overlays can
label the editor placement differently from the card menu.

* fix: fail loud on undecryptable KB embedding key with recovery path (#13806)

* fix: fail loud on undecryptable KB embedding key with recovery path

- Add KBKeyDecryptError exception for SECRET_KEY rotation scenarios
- Implement require_api_key flag in load_kb_metadata for critical vs non-critical paths
- Retrieval: raise when component doesn't supply key, allow component key bypass
- Ingestion: raise when no component key, warn when component key used as fallback
- Add comprehensive test coverage for decrypt failure scenarios
- Consolidate _kb_paths imports in knowledge.py

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

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

* chore: trigger CI

* [autofix.ci] apply automated fixes

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

* [autofix.ci] apply automated fixes

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

---------

Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* feat(models): add Azure AI Foundry to unified model provider setup (#13912)

* feat(models): add Azure AI Foundry to unified model provider setup

Expose Azure AI…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation lgtm This PR has been approved by a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants