Skip to content

Adding no-BYOC support for Flower integration#4514

Open
d0uwe wants to merge 3 commits intoNVIDIA:mainfrom
d0uwe:main
Open

Adding no-BYOC support for Flower integration#4514
d0uwe wants to merge 3 commits intoNVIDIA:mainfrom
d0uwe:main

Conversation

@d0uwe
Copy link
Copy Markdown
Contributor

@d0uwe d0uwe commented May 4, 2026

Description

This PR adds support for pre-deployed Flower application code in NVFlare's Flower integration, eliminating the need for BYOC (Bring Your Own Code) authorization in production environments.

Key Changes

New Feature: Pre-deployed Mode

  • Added flower_app_path parameter to FlowerJob, FlowerRecipe, FlowerController, FlowerExecutor, and FlowerServerApplet
  • When using flower_app_path, the Flower app code is not packaged into the job ZIP (no custom/ directory)
  • The job ZIP does not require BYOC authorization on any participant
  • Important: The path only needs to exist on the server. Clients receive the Flower app from the server via Flower's FAB (Flower Application Bundle) distribution mechanism, so client participants do not need a pre-existing copy. Unfortunately there is currently not yet a way in Flower itself to disable BYOC, so the clients do still need to trust the server admin that sets up the server, but it's a step forward compared to only having BYOC as an option.

Backwards Compatibility

  • Existing flower_content parameter continues to work exactly as before (BYOC mode)
  • Users can choose either mode:
    • BYOC mode (flower_content): Package Flower app in job ZIP (requires BYOC authorization)
    • Pre-deployed mode (flower_app_path): Server-only deployment (no BYOC needed)

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • Breaking change (fix or new feature that would cause existing functionality to change).
  • New tests added to cover the changes.
  • Quick tests passed locally by running ./runtest.sh.
  • In-line docstrings updated.
  • Documentation updated.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR adds a "pre-deployed mode" to NVFlare's Flower integration, allowing the Flower app to live at a server-side path (flower_app_path) instead of being bundled into the job ZIP via BYOC. The two modes are mutually exclusive, validated at construction time, and the server distributes the app to clients via Flower's FAB mechanism, so no client-side path is needed.

Confidence Score: 5/5

Safe to merge — only a single P2 style issue (unused import) was found; all functional paths are correctly guarded and well tested.

No P0 or P1 issues found. The feature is implemented correctly across the call stack, validation is in place, and the new test suite covers BYOC mode, pre-deployed mode, mutual-exclusion errors, and export integration. The only finding is an unused Optional import in executor.py.

nvflare/app_opt/flower/executor.py (unused import); nvflare/job_config/fed_job_config.py (pre-existing _copy_file_sources file-branch gap already noted in prior review)

Important Files Changed

Filename Overview
nvflare/app_opt/flower/flower_job.py Adds flower_app_path parameter with mutual-exclusion validation; conditionally skips to_server/to_clients for BYOC content when using pre-deployed mode. Logic is correct.
nvflare/app_opt/flower/applet.py Adds flower_app_path to FlowerServerApplet with runtime directory validation; client applet is deliberately unchanged with an explanatory docstring noting FAB distribution.
nvflare/app_opt/flower/executor.py Only change is adding an unused from typing import Optional import; no functional changes needed since clients receive the app via FAB.
nvflare/app_opt/flower/controller.py Threads flower_app_path through to FlowerServerApplet via get_applet(); straightforward plumbing change.
nvflare/app_opt/flower/recipe.py Makes flower_content optional, adds flower_app_path, and forwards it to _create_flower_job; includes updated docstrings and a pre-deployed mode example.
nvflare/job_config/fed_job_config.py Removes unconditional os.makedirs(custom_dir) so the directory is created on-demand; _copy_file_sources file-copy branch still lacks an os.makedirs guard (noted in prior review thread).
tests/unit_test/recipe/flower_job_test.py New test file covering BYOC, pre-deployed, validation errors, and controller propagation of flower_app_path; good coverage.
tests/unit_test/recipe/flower_recipe_test.py Extends existing recipe tests with pre-deployed path, mutual-exclusion, and full export integration tests; coverage is thorough.
docs/hello-world/hello-flower/index.rst Documentation updated with BYOC vs pre-deployed code examples and a new simulation section for pre-deployed mode.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[FlowerJob / FlowerRecipe] -->|flower_content set| B[BYOC Mode]
    A -->|flower_app_path set| C[Pre-deployed Mode]
    A -->|both or neither| D[ValueError raised]

    B --> E[to_server: copies app into job ZIP custom/]
    B --> F[to_clients: copies app into job ZIP custom/]
    E --> G[FlowerServerApplet\nuses custom_dir from workspace]
    F --> H[FlowerClientApplet\nuses custom_dir from workspace]

    C --> I[FlowerController\npasses flower_app_path to applet]
    I --> J[FlowerServerApplet\nvalidates path exists on server\nsets flower_app_dir = flower_app_path]
    J --> K[Clients receive app\nfrom server via FAB]
Loading

Reviews (2): Last reviewed commit: "updating tests for flower no-BYOC" | Re-trigger Greptile

Comment on lines 157 to +160
config_dir = os.path.join(job_dir, app_name, CONFIG)
custom_dir = os.path.join(job_dir, app_name, CUSTOM)
os.makedirs(config_dir, exist_ok=True)
os.makedirs(custom_dir, exist_ok=True)
# custom_dir will be created on-demand if custom code is added.
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.

P1 Potential regression: custom_dir not created before _copy_file_sources

Removing the unconditional os.makedirs(custom_dir, exist_ok=True) leaves custom_dir uncreated for jobs whose all components come from FL_PACKAGES (no user-defined classes), have no ext_scripts, and no ext_dirs, but do have a file_sources entry with app_folder_type="custom", dest_dir=None, and a file (not a directory) as the source. In _copy_file_sources, when dest_dir is None and os.path.isfile(src_path) is True, shutil.copy(src_path, dest_file) is called where dest_file is inside custom_dir. If custom_dir does not yet exist, shutil.copy raises FileNotFoundError. The shutil.copytree (directory) branch is safe because dirs_exist_ok=True creates the destination, but the file-copy branch has no such fallback.

Suggested change
config_dir = os.path.join(job_dir, app_name, CONFIG)
custom_dir = os.path.join(job_dir, app_name, CUSTOM)
os.makedirs(config_dir, exist_ok=True)
os.makedirs(custom_dir, exist_ok=True)
# custom_dir will be created on-demand if custom code is added.
os.makedirs(config_dir, exist_ok=True)
os.makedirs(custom_dir, exist_ok=True)
# custom_dir created eagerly; _copy_file_sources needs it for file (non-dir) sources.

Copy link
Copy Markdown
Contributor Author

@d0uwe d0uwe May 4, 2026

Choose a reason for hiding this comment

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

How should we handle this? I noticed that if the custom_dir gets created, there always is an empty custom dir, which then triggers the byoc check, even though the directory is empty.

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.

The right fix is to add os.makedirs(dest_path, exist_ok=True) just before the shutil.copy call in _copy_file_sources. This way custom_dir is only created when there's actual content going into it — no empty directory, no spurious BYOC trigger.

The dest_dir is not None branch already does this correctly. The dest_dir is None (file-copy) branch just needs the same treatment:

Suggested change
config_dir = os.path.join(job_dir, app_name, CONFIG)
custom_dir = os.path.join(job_dir, app_name, CUSTOM)
os.makedirs(config_dir, exist_ok=True)
os.makedirs(custom_dir, exist_ok=True)
# custom_dir will be created on-demand if custom code is added.
def _copy_file_sources(self, config_dir, custom_dir, file_sources):
for s in file_sources:
# s is a tuple of (src_path, dest_dir)
src_path, dest_dir, app_folder_type = s
if app_folder_type == "config":
target_dir = config_dir
else:
target_dir = custom_dir
if dest_dir:
dest_path = os.path.join(target_dir, dest_dir)
if not os.path.exists(dest_path):
os.makedirs(dest_path, exist_ok=True)
else:
dest_path = target_dir
if os.path.isfile(src_path):
base_name = os.path.basename(src_path)
dest_file = os.path.join(dest_path, base_name)
os.makedirs(dest_path, exist_ok=True)
shutil.copy(src_path, dest_file)
else:
# this is a dir
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)

shutil.copytree with dirs_exist_ok=True already handles directory creation, so that branch is fine. Only the file-copy branch needs the guard. config_dir is always pre-created so the makedirs is only a real operation when target_dir is custom_dir.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Comment thread nvflare/app_opt/flower/applet.py Outdated
Comment thread tests/unit_test/recipe/flower_recipe_test.py Outdated
@holgerroth
Copy link
Copy Markdown
Collaborator

@d0uwe Thanks for the contribution. I think the feature is useful, especially for production Flower deployments where the app is pre-installed and should not be packaged into the NVFlare job ZIP. But I’m concerned about describing this as “no-BYOC.”

With this PR, Flower app code is no longer carried through NVFlare custom/, but clients can still receive and execute app code through Flower’s FAB distribution mechanism. That means “BYOC disabled” would no longer clearly mean “no job-specific executable code can reach clients.”

Could we frame this as “server-predeployed Flower app mode” instead of “no-BYOC,” and add an explicit policy/guardrail for it? For example, Flower FAB execution could require a separate site opt-in when BYOC/custom-code execution is disabled. Ideally, flower_app_path should also be constrained to admin-approved locations or app registrations, rather than being an arbitrary path from the job definition.

I’m supportive of the use case, but I think we should preserve NVFlare’s security contract: disabling BYOC should not silently allow another dynamic code path to clients unless the site explicitly opts into that trust model.

@IsaacYangSLA, @nvidianz, @pcnudde for viz

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants