feat(integrations): add the lfx-slack bundle with Web API actions on connections - #14962
feat(integrations): add the lfx-slack bundle with Web API actions on connections#14962erichare wants to merge 9 commits into
Conversation
Slack is the first provider whose user and bot tokens share scope names -- chat:write is both a User Token Scope and a Bot Token Scope -- so a bundle capability that must run as the app's bot user cannot tell from granted_scopes whether it was handed a user-token connection. The connection row already records executing_identity, but the resolver dropped it on the floor when building ResolvedCredential, leaving Slack's own not_allowed_token_type error as the only protection, after the request was already sent. Add an additive optional ResolvedCredential.identity and populate it in DatabaseConnectionResolverService._resolved_from_row. The headless env resolver leaves it None, which callers read as "the operator vouched for this token". No existing field changed, so BUNDLE_API_VERSION stays 1; the changelog entry is required because lfx/integrations is an in-scope surface. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Creates src/bundles/slack (distribution lfx-slack) with the seven actions the
INT-1 discovery gate froze in design/dedicated-integrations/matrices/slack.json:
four user-identity actions (search, read thread, send, create canvas) and three
bot-identity actions (post, add reaction, list channel members). All seven run
on the Slack Web API through slack_sdk's AsyncWebClient, per the accepted
substrate decision -- no MCP in this bundle.
capabilities.v1.json is lifted mechanically from the matrix: the same action
ids, display names, identities, required and conditional scopes, deployment
contexts and component classes, plus the two named auth profiles the connection
contract requires (slack-user-oauth, slack-bot-install) so a bot capability can
never resolve through a user-token connection by accident. Bot capabilities
declare no desktop context, because Slack desktop redirects may not request bot
scopes; the actual hiding is discovery's job (INT-7/INT-8), not the bundle's.
Two things needed care:
* Slack answers HTTP 200 with {"ok": false, "error": ...}, so lfx's
status-code-only fallback would report an expired token or a missing scope as
provider-unavailable. The bundle registers a Slack error normalizer that maps
the ok:false codes onto auth-expired / scope-missing / rate-limited /
action-unsupported, and an auth rejection spends the one reactive re-resolve
the lease allows -- which is how a rotated Slack token is picked up, since
Slack tokens otherwise never expire.
* User and bot scopes share names, so the components compare
ResolvedCredential.identity and fail closed before the first request.
_base.py, _chat.py and _client.py sit at the package root rather than in the
bundle directory: `lfx extension validate` scans that directory and reports a
shared abstract Component base as build-method-missing.
Tests patch slack_sdk's lowest transport layer with recorded fixtures, so the
SDK's real request assembly, response parsing and error raising stay under
test while aiohttp is never touched. Sample workspace IDs are deliberately
non-hex (C0SLACKDEMO, not C0123456789) so detect-secrets does not read them as
high-entropy strings. The opt-in live-workspace suite is marked
api_key_required and self-skips without LANGFLOW_SLACK_LIVE_*.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Runs the same checklist lfx-google went through when it graduated to a default-install bundle: root pyproject markers (deps, workspace source, member) plus uv.lock; the enterprise-hardened bundle profile and its recompiled lock; the python-default and python-full release-inventory rows, including the source_roots entry test_contract_required_files_exist_in_sources needs to resolve those required_files; migration-table rows for the seven component classes; the Slack sidebar group (the Slack icon already existed); and the docs pages, the bundle list, the graduated-install map and the docs sidebar. Two seams the checklist does NOT need: * component_index.json indexes lfx.components only, so bundle components never enter it and `make build_component_index` is a no-op here. * Backend locales/en.json is regenerated from lfx.components on every PR that touches src/bundles/**, and reaches bundle components only through the lfx.components.<provider> compatibility shims that exist for components that MOVED out of the tree. lfx-slack is new and has no shim, so hand-written keys would be deleted by the next gp-backend-check run. Slack strings therefore stay untranslated for now, exactly as paddle, confluent, valkey and nextplaid do. docs/docs/Develop/connection-oauth.mdx was never registered in the sidebar, so the customer-owned Slack app guide the ticket asks for was unreachable. It is now listed under Develop and carries the redirect URLs, the per-action scope table for both identities, and a worked two-registration example. Links to the new pages are relative, because absolute slugs resolve against the released docs version, which does not have them yet. scripts/ci/check_capability_manifests.py is new and generic over every bundle that declares `integrations`: it proves each shipped capabilities.v1.json still agrees with its discovery-gate matrix on ids, identity, scopes, deployment contexts and component class. Nothing else stops those two from drifting, and a scope present in only one of them is either unreviewed or never requested at consent time. Extra capabilities and auth profiles are allowed so TRG-5 can add its app-token profile on top. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
run_action memoized the Slack response on the component instance so a component with two outputs (Messages plus Pagination) cost one call against Slack's per-method rate tier. Nothing ever cleared it, and the graph reuses one component instance across builds: Graph._set_cache_to_vertices_in_cycle forces output.cache=False on every cycle vertex so its outputs recompute each iteration, while Vertex.build keeps the same custom_component. A Slack write action inside a Loop therefore posted once and then reported that first response for every later iteration -- N green statuses, one message in the channel. Component._build_results calls _pre_run_setup once per build, so drop the memo there. Sharing within one build is unchanged; freshness between builds is restored. test_slack_rebuild.py covers both the framework hook and the public build methods, and asserts the two-output case still costs one call. Also move require_identity inside the integration_action span. The fail-closed identity denial is the one an operator most wants counted, and outside the context manager it emitted no integration_action telemetry at all. The guard still runs before any HTTP request; SlackClient construction is inert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three review follow-ups, no behaviour change. check_capability_manifests.py compares ids, identity, scopes, contexts and component_ref, but it cannot compare field names: the capability manifest carries no schema, only the matrix does. test_slack_matrix_schema.py closes that gap by comparing the shipped components' input and output names against design/dedicated-integrations/matrices/slack.json, with the four deliberate deviations listed explicitly (the emoji_name rename, and the three groups of scalar outputs folded into one Data). The deviation lists are asserted exhaustive, so amending the matrix later fails here until this file follows. test_oauth_providers.py now pins that the bundle's two auth profiles declare the same authorize and token URLs that providers.endpoints hardcodes. They agree today; nothing stopped one side from drifting and sending a user to one authorization server while the broker redeemed the code at another. docs: connection-oauth.mdx's Slack redirect-URL and scope material moves to a new trailing section, so this page stays strictly append-only for the other integration branches that also extend it. bundles-slack.mdx links Data to /data-types#json, the anchor the current docs version actually defines; #data exists only as a raw <a id> in the pre-1.9 versioned copies and was the only broken anchor the Docusaurus build reported on the next version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Restack INT-12 (lfx-slack) onto INT-11 (lfx-microsoft) so the Slack PR targets the Microsoft branch instead of the INT-5 tip. Both bundles were built independently from 5615cd0 and touch the same registration seams, so the conflicts were expected; both bundles are registered here. Conflicts resolved, INT-11's entry first in every append-only list: - pyproject.toml: lfx-microsoft then lfx-slack in the bundle dependency list, the uv workspace sources table, and the workspace members list. - scripts/ci/bundle_profiles.json, scripts/ci/release_inventory_contract.json: both distributions, alphabetical where the surrounding list is sorted and append-ordered where it is not. - src/lfx/src/lfx/extension/migration/migration_table.json: both row sets appended (991 -> 1027; +8 microsoft, +28 slack). Append-only against both the INT-11 tip and 5615cd0. - docs/docs/Develop/connection-oauth.mdx: both trailing sections kept, the Microsoft 365 section first. - docs/sidebars.js: both branches added the same "Develop/connection-oauth" entry. Kept INT-11's placement, inside "Authentication and authorization" beside the Entra runbook it links to, and dropped INT-12's duplicate. - scripts/ci/bundle_profile_locks/enterprise-hardened.lock.json: not hand-merged. Regenerated with `manage_bundle_profiles.py compile --output-dir` after the other conflicts were resolved; the result is INT-11's lock plus the lfx-slack row and a new profile_digest. - uv.lock: git's merge produced a lock `uv lock --offline` and `uv lock --check` both accept unchanged (857 packages, both bundles editable). No re-resolution was needed and nothing had to be fetched. Two fixes the combined tree needs that neither branch could make alone: - INT-12's new generic checker, scripts/ci/check_capability_manifests.py, reaches INT-11's manifest for the first time here and rejected it on nine counts. Eight are INT-11's deliberate `offline_access` deviation, which is load-bearing (Entra never echoes the scope, and the resolver computes `required - granted` literally, so transcribing the matrix would fail every Microsoft resolution) and is already pinned bundle-side; the ninth is the short "Microsoft 365" provider label the bundle ships everywhere against the matrix's longer one. Rather than change either bundle's shipped behaviour, the checker gained a DELIBERATE_DEVIATIONS table holding the exact expected values, so any other drift still fails and a deviation that has converged is reported as stale. Mutation-checked: dropping either entry reproduces the 8 and 1 failures. - INT-11's docs link to /bundles-microsoft, /entra-app-registration and /connection-oauth absolutely; those slugs exist only on the `next` version, so `npm run build` failed with four broken links. Converted to relative .mdx links, the form INT-12 already had to use for /bundles-slack. The build now passes with zero broken links and no broken anchor on `next`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
✅ Test Coverage AdvisorNo source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉
|
This comment has been minimized.
This comment has been minimized.
…anch Takes INT-11's fix to scripts/gp/extract_backend_strings.py, which now also walks installed extension bundles through lfx.extension.load_installed_extensions(). locales/en.json conflicted (both branches carried a bot regeneration of it) and is resolved the only honest way: by re-running the extractor rather than by hand-merging either side. That adds the 101 rows for this bundle's seven Slack components, which the previous extractor could not see -- lfx-slack ships no lfx/components/slack shim, and scripts/ci/check_bare_names.py would reject one. Nothing is removed and no value changes relative to the merged-in microsoft branch; --check is clean immediately after the run.
|
Build successful! ✅ |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/int-11-lfx-microsoft #14962 +/- ##
=============================================================
- Coverage 65.26% 65.21% -0.05%
=============================================================
Files 2519 2520 +1
Lines 263172 263190 +18
Branches 36824 39272 +2448
=============================================================
- Hits 171763 171649 -114
- Misses 89189 89321 +132
Partials 2220 2220
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
feat(integrations): add the lfx-slack bundle with Web API actions on connections
Base:
feat/int-11-lfx-microsoft(INT-11). That branch bases onfeat/int-5-oauth-broker(#14935); retarget torelease-1.13.0once #14906..#14935 and INT-11 merge.Restacked on INT-11
This branch was built independently from the INT-5 tip (
5615cd03a3) and then merged withfeat/int-11-lfx-microsoft, which shares every bundle registration seam. Review this PR's own diffagainst
feat/int-11-lfx-microsoft, not againstfeat/int-5-oauth-broker— the merge commit carriesthe resolutions and its message lists them one by one. In short: INT-11's entry comes first in every
append-only list (
pyproject.tomlx3,bundle_profiles.json,release_inventory_contract.json,migration_table.json,connection-oauth.mdx); the duplicated"Develop/connection-oauth"sidebarentry was reduced to INT-11's copy inside Authentication and authorization, beside the Entra
runbook it links to;
enterprise-hardened.lock.jsonwas regenerated withmanage_bundle_profiles.py compile, not hand-merged; anduv.lockneeded no re-resolution(
uv lock --offlineanduv lock --checkboth accept it unchanged, 857 packages, nothing fetched).Two problems only the combined tree can surface, both fixed in the merge commit:
scripts/ci/check_capability_manifests.py— added by this PR, generic over every bundle — reachedlfx-microsoft's manifest for the first time and rejected it nine times. Eight are INT-11'sdeliberate
offline_accessdeviation (Entra never echoes the scope and the resolver computesrequired - grantedliterally, so transcribing the matrix would fail every Microsoft resolution;already pinned by
src/bundles/microsoft/tests/test_capabilities_manifest_contract.py); the ninth isthe short
"Microsoft 365"provider label the bundle ships everywhere against the matrix's longerone. Neither bundle's shipped behaviour was changed. Instead the checker gained a
DELIBERATE_DEVIATIONStable recording the exact expected values, so any other drift still failsand a deviation that has since converged is reported as stale rather than silently accepted.
Mutation-checked, and covered by six new tests.
/bundles-microsoft,/entra-app-registrationand/connection-oauthabsolutely; those slugs exist only on the
nextversion, sodocs $ npm run buildfailed with fourbroken links. Converted to relative
.mdxlinks — the same form this PR already had to use for/bundles-slack(see the note below). Worth folding back into INT-11 if that branch moves.Summary
src/bundles/slack(distributionlfx-slack) with the seven actions the INT-1 discovery gate froze indesign/dedicated-integrations/matrices/slack.json:Slack: Search / Read Thread / Send Message / Create Canvas (as user)andSlack: Post Message / Add Reaction / List Channel Members (as app).slack_sdk, perdecisions/substrate-slack.md(Option B) and the matrix'ssubstrate_notes.slack_sdkandaiohttpare new dependencies;aiohttpis declared explicitly becauseAsyncWebClientimports it andcross-bundle-test.ymlinstalls only declared deps.capabilities.v1.jsonis lifted mechanically from the matrix (ids, display names, identities, required and conditional scopes, deployment contexts, component classes) and declares the two named auth profiles the connection contract requires:slack-user-oauthandslack-bot-install. Policy keys areintegrations.slack.<user|bot>.<verb>.HTTP 200with{"ok": false, "error": ...}, so lfx's status-code-only fallback would have reported an expired token or a missing scope asprovider-unavailableand the frontend's code-keyed reconnect/grant affordances would never fire.invalid_auth/token_expired/token_revoked/account_inactive/not_authed→auth-expired;missing_scope(withneeded) →scope-missing;ratelimited/HTTP 429 →rate-limitedwithRetry-After;not_allowed_token_type/channel_not_found/… →action-unsupported; everything else →provider-unavailable.ResolvedCredential.identity(lfx) populated from the connection row'sexecuting_identity(langflow-base). Slack user and bot tokens share scope names —chat:writeis both a User Token Scope and a Bot Token Scope — sogranted_scopescannot tell them apart. A bot capability handed a user connection (or the reverse) now fails closed withconnection-not-authorizedbefore the first HTTP request.BUNDLE_API_VERSIONstays1; the changelog entry is required only becauselfx/integrationsis an in-scope surface.auth-expiredrejection spends the one reactive re-resolveCredentialLeaseallows and retries once. That is how a rotated Slack token is picked up: Slack tokens do not expire unless the app opted into rotation, so a rejection is the only signal.pyproject.tomlmarkers +uv.lock;bundle_profiles.json(enterprise-hardened) + recompiledenterprise-hardened.lock.json;release_inventory_contract.jsonforpython-defaultandpython-full, plus thesource_rootsentrytest_contract_required_files_exist_in_sourcesneeds;migration_table.jsonrows for the seven classes; theSlacksidebar group (the icon already existed); docs page, bundle list, graduated-install map, docs sidebar.scripts/ci/check_capability_manifests.py(+ tests), generic over every bundle that declaresintegrations: it proves each shippedcapabilities.v1.jsonstill agrees with its discovery-gate matrix. Extra capabilities and auth profiles are tolerated so TRG-5 can add itsslack-app-tokenprofile on top._pre_run_setup, the framework's per-build reset hook. Without that, a Slack action inside aLoop(or any rebuilt vertex —Graph._set_cache_to_vertices_in_cycleforcesoutput.cache=FalsewhileVertex.buildreuses onecustom_component) would run once and then report that first response for every later iteration: N green statuses, one message in the channel. Sharing between a component's two outputs within one build is unchanged.integration_actionspan, so a fail-closedconnection-not-authorizeddenial is counted in telemetry like any other integration error. It still fires before any HTTP request.docs/docs/Develop/connection-oauth.mdxin the docs sidebar — it was unreachable — and extends its Slack section with both redirect URL forms, the per-action scope table for each identity, and a worked two-registrationLANGFLOW_CONNECTION_OAUTH_REGISTRATIONSexample.Verification
Bundle suite, run exactly the way
.github/workflows/cross-bundle-test.ymldoes (a clean venv holding only in-repolfx+ the bundle + pytest), on both PR-matrix Python legs:The 3 deselected are the opt-in live-workspace suite (
api_key_required).lfx package suite (isolated, from
src/lfx):(The 2 skips are
test_pilot_slack_upgrade.py's distribution checks, which self-skip whenlfx-slackis absent from the isolated lfx venv — the same waytest_pilot_paddle_upgrade.pydoes.)Backend and release tooling, from the repo root:
(The 4 backend skips are pre-existing and unrelated:
test_build_component_index.pyself-skips with "build_component_index.py script not found".)The release-inventory rows were also checked against a real built wheel, since
check_release_inventory.pyneeds an installed wheel shape (in an editable checkout it reports missingrequired_filesfor every bundle, lfx-openai and lfx-google included):Frontend and docs:
Ruff (
check+format --check) is clean on every changed Python path;detect-secrets-hookis clean with no.secrets.baselineadditions.Notes for the reviewer
Graph.execution_principaldefaults toExecutionPrincipal.unknown(), andauthorize_principaldeniesunknownfor user and instance connections, so every Slack component run from the canvas,/api/v1/run, or a deployment fails closed until INT-6 stamps the principal. Onlylfx runwithLF_CONNECTION__SLACK__<NAME>works end to end today. INT-8 likewise has to ship theconnection_refrenderer before the connection field is settable from the canvas; until then set it through tweaks or the API. The bundle is fully unit-tested against a fake resolver.tools/listfixture to one existing capability (slack.user.searchis the natural target, and keeps itscomponent_ref). This PR ships no MCP substrate, andsubstrate-slack.md/estimate.mdare amended by INT-9's PR, not this one.slack-app-tokenprofile to the samecapabilities.v1.json. The new manifest checker deliberately allows extra profiles and capabilities so that lands without a checker change.SLACK_API_BASE_URLis a module constant and no component exposes a URL, host, or proxy input, so there is no user-controllable request target.lfx.utils.ssrf_transportbuilds httpx clients with DNS pinning andAsyncWebClientspeaks aiohttp, for which lfx ships no equivalent; removing the surface entirely is the stronger guarantee. Two tests pin it (the constant, and "no component declares a request-target input"). If we would rather add an aiohttp connector tolfx.utils.ssrf_transport, that is a BUNDLE_API-surface change and belongs in its own PR.locales/en.jsonis deliberately untouched.scripts/gp/extract_backend_strings.pywalkslfx.componentsand reaches bundle components only through thelfx.components.<provider>compatibility shims that exist for components that moved out of the tree.lfx-slackis new and has no shim, andgp-backend-check.ymlregenerates and auto-commitsen.jsonon every PR touchingsrc/bundles/**, so hand-written Slack keys would be deleted on the next run. paddle, confluent, valkey and nextplaid have no keys either."Develop/connection-oauth"entry todocs/sidebars.js. The restack keeps one copy — INT-11's, inside Authentication and authorization — so this PR no longer touches that part of the sidebar at all. Everything else this PR adds to a shared file is append-only and alphabetically placed.docs/docs/Develop/connection-oauth.mdxis extended strictly by appending. The Slack redirect-URL and per-action scope material is a new trailing section, per the cross-ticket convention for this page; no existing text is edited.slackandmicrosoftrows inextensions-bundle-list.mdxlink relatively on purpose. Every other row uses an absolute slug, but/bundles-slackand/bundles-microsoftexist only on thenextdocs version, so the absolute form fails the build:Docusaurus found broken links! - Broken link on source page path = /next/extensions-bundle-list. Verified by trying it. Normalize it only when 1.13 is cut and the slug exists in a released version.schema.outputs, all deliberate: scalar rows (has_more,next_cursor,canvas_id) are folded into oneDataoutput per component, because Langflow edges are typed and a barebool/stroutput cannot be consumed downstream. Thereactions.addinput the matrix callsnameis exposed asemoji_name, becauseComponent.nameis the registry-name override and an input callednamewould be silently shadowed by the class attribute; the Web API parameter is still sent asname.src/bundles/slack/tests/test_slack_matrix_schema.py, which compares the shipped components' input and output names againstmatrices/slack.jsonwith the deviations listed explicitly and asserted exhaustive.check_capability_manifests.pycannot do this itself: the capability manifest carries no schema, only the matrix does.src/backend/tests/unit/services/connection/test_oauth_providers.pynow pins that the bundle's two auth profiles declare the same authorize and token URLsproviders.endpointshardcodes, so INT-8's picker and the broker cannot drift apart silently._base.py,_chat.py,_client.pysit at the package root, not incomponents/slack/:lfx extension validatescans the bundle directory and reports a shared abstractComponentbase there asbuild-method-missing.C0SLACKDEMO, notC0123456789) sodetect-secretsdoes not read them as high-entropy strings..secrets.baselineneeded no new entries.tests/test_slack_live.pyhas never run against real Slack. It is markedapi_key_required, self-skips withoutLANGFLOW_SLACK_LIVE_USER_TOKEN/LANGFLOW_SLACK_LIVE_BOT_TOKEN/LANGFLOW_SLACK_LIVE_CHANNEL, and is deselected in CI. The hosted Langflow-owned Slack apps (confidential + a second PKCE app for Desktop) and the Marketplace listing that lifts theconversations.repliesrate tier remain external dependencies.Follow-ups (not in this PR)
oss-versionre-pin once the INT stack merges, which also re-verifiesenterprise-hardened.lock.jsonthroughscripts/check-oss-compat.sh. EEdocs/connectors.mdcurrently over-claims first-party Slack support and should be reconciled with these seven actions at the same time.policy_keys; INT-7/INT-8 deliver the Desktop hiding of bot actions by filtering ondeployment_contexts. This PR only declares them.locales/en.jsonkeys for the seven components. DECISIONS.md assigns the fix: include Google as a default partner bundle #14220 locale mirroring to INT-11; see the note above for why hand-writing them here would be undone bygp-backend-check.yml. Until INT-8's baseline lands, the Slack palette strings are English-only, as paddle's and confluent's are.python -c "import lfx_slack"raisesImportErrorfrom a partially initializedlfx.custom.custom_component.component, whileimport lfx_googlesucceeds. It reproduces with no Slack code at all (import lfx.io, lfx.schema.data; from lfx.custom.custom_component.component import Component) and does not affect the real loader path —load_installed_extensionsreportslfx-slack ok, 7 components, 0 errors.Post-review changes:
354709002emerges INT-11's fix toscripts/gp/extract_backend_strings.py— the extractor now also walks installed extension bundles throughlfx.extension.load_installed_extensions()— which closes the "Backendlocales/en.jsonkeys for the seven components" follow-up above.locales/en.jsonconflicted on the merge and was resolved by re-running the generator rather than by hand-merging either side, so the seven Slack components' 101 rows are generated and the auto-bake job will keep them instead of deleting them. Nothing is removed and no value changes relative to the merged-in branch;--checkis clean immediately after the run.src/bundles/slack/testsin the clean lfx-only venv is unchanged at 56 passed / 3 deselected.