Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1f6f7a4
feat(framework): Add flower-agent
danielnugraha Apr 22, 2026
ae795a5
Apply suggestion from @Copilot
danielnugraha Apr 22, 2026
34e8ec1
Format
danielnugraha Apr 22, 2026
25a7d92
Merge remote-tracking branch 'refs/remotes/origin/add-flwr-agent' int…
danielnugraha Apr 22, 2026
f7557de
Format
danielnugraha Apr 22, 2026
b548143
Update framework/py/flwr/supercore/cli/flower_agent.py
danielnugraha Apr 22, 2026
0fd81a9
Rename
danielnugraha Apr 22, 2026
021d667
Format
danielnugraha Apr 22, 2026
5021759
Format
danielnugraha Apr 22, 2026
e970ef3
Refactor
danielnugraha Apr 22, 2026
434a5f9
Format
danielnugraha Apr 22, 2026
48ff2ed
Refactor to agentapp
danielnugraha Apr 22, 2026
63a7b34
Refactor telemetry name
danielnugraha Apr 22, 2026
f0741de
Refactor to agentapp
danielnugraha Apr 22, 2026
8a1ad14
Pylint fix
danielnugraha Apr 22, 2026
5031b35
feat(framework): Add flwr-model
danielnugraha Apr 22, 2026
a785c57
feat(framework): Add flwr-connector
danielnugraha Apr 22, 2026
fe6f709
Format
danielnugraha Apr 22, 2026
817fa69
Format
danielnugraha Apr 22, 2026
e1b6f26
Format
danielnugraha Apr 22, 2026
51eefc1
Rename dir
danielnugraha Apr 23, 2026
706ac49
Rename dir
danielnugraha Apr 23, 2026
f77aa13
Rename dir
danielnugraha Apr 23, 2026
bc04644
Rename
danielnugraha Apr 23, 2026
9d69031
Merge branch 'add-flwr-agent' into add-flwr-model
danielnugraha Apr 23, 2026
17371b7
Rename
danielnugraha Apr 23, 2026
242f584
Merge branch 'add-flwr-model' into add-flwr-connector
danielnugraha Apr 23, 2026
278d92d
Rename
danielnugraha Apr 23, 2026
5b6d1b1
Rename
danielnugraha Apr 23, 2026
3e29c84
Merge branch 'add-flwr-agent' into add-flwr-model
danielnugraha Apr 23, 2026
5e210d9
Rename
danielnugraha Apr 23, 2026
c16871c
Merge branch 'main' into add-flwr-model
danielnugraha Apr 23, 2026
e205c8e
Remove test
danielnugraha Apr 23, 2026
8b8fe88
Merge remote-tracking branch 'refs/remotes/origin/add-flwr-model' int…
danielnugraha Apr 23, 2026
ea9f3f7
Merge branch 'add-flwr-model' into add-flwr-connector
danielnugraha Apr 23, 2026
0a4a4d4
Remove test
danielnugraha Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions framework/py/flwr/common/exit/exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ def _try_obtain_telemetry_event() -> EventType | None:
return EventType.RUN_SUPERNODE_LEAVE
if sys.argv[0].endswith("flwr-agentapp"):
return EventType.FLWR_AGENTAPP_RUN_LEAVE
if sys.argv[0].endswith("flwr-model"):
return EventType.FLWR_MODEL_RUN_LEAVE
if sys.argv[0].endswith("flwr-connector"):
return EventType.FLWR_CONNECTOR_RUN_LEAVE
if sys.argv[0].endswith("flwr-serverapp"):
return EventType.FLWR_SERVERAPP_RUN_LEAVE
if sys.argv[0].endswith("flwr-clientapp"):
Expand Down
8 changes: 8 additions & 0 deletions framework/py/flwr/common/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ def _generate_next_value_(name: str, start: int, count: int, last_values: list[A
FLWR_AGENTAPP_RUN_ENTER = auto()
FLWR_AGENTAPP_RUN_LEAVE = auto()

# CLI: flwr-model
FLWR_MODEL_RUN_ENTER = auto()
FLWR_MODEL_RUN_LEAVE = auto()

# CLI: flwr-connector
FLWR_CONNECTOR_RUN_ENTER = auto()
FLWR_CONNECTOR_RUN_LEAVE = auto()

# --- Simulation Engine ------------------------------------------------------------

# Python API: `run_simulation`
Expand Down
4 changes: 4 additions & 0 deletions framework/py/flwr/supercore/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@

from .flower_superexec import flower_superexec
from .flwr_agentapp import flwr_agentapp
from .flwr_connector import flwr_connector
from .flwr_model import flwr_model

__all__ = [
"flower_superexec",
"flwr_agentapp",
"flwr_connector",
"flwr_model",
]
76 changes: 76 additions & 0 deletions framework/py/flwr/supercore/cli/flwr_connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""`flwr-connector` command."""


import argparse
from logging import DEBUG, INFO
from queue import Queue

from flwr.common.args import add_args_flwr_app_common
from flwr.common.constant import SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS
from flwr.common.exit import ExitCode, flwr_exit
from flwr.common.logger import log, mirror_output_to_queue, restore_output
from flwr.supercore.executors.run_connector import run_connector


def flwr_connector() -> None:
"""Run process-isolated Flower ConnectorApp."""
args = _parse_args_run_flwr_connector().parse_args()

if not args.insecure:
flwr_exit(
ExitCode.COMMON_TLS_NOT_SUPPORTED,
"`flwr-connector` does not support TLS yet.",
)

# Capture stdout/stderr
log_queue: Queue[str | None] = Queue()
mirror_output_to_queue(log_queue)

log(INFO, "Start `flwr-connector` process")
log(
DEBUG,
"`flwr-connector` will attempt to connect to SuperLink's "
"ServerAppIo API at %s",
args.serverappio_api_address,
)
run_connector(
serverappio_api_address=args.serverappio_api_address,
log_queue=log_queue,
token=args.token,
certificates=None,
parent_pid=args.parent_pid,
runtime_dependency_install=args.runtime_dependency_install,
)

# Restore stdout/stderr
restore_output()


def _parse_args_run_flwr_connector() -> argparse.ArgumentParser:
"""Parse `flwr-connector` command line arguments."""
parser = argparse.ArgumentParser(
description="Run a Flower ConnectorApp",
)
parser.add_argument(
"--serverappio-api-address",
default=SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
type=str,
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The help= string concatenation is missing a separating space/newline between sentences, so argparse --help will render as ...).By default, .... Consider adding an explicit space at the end of the first literal (or using a single combined literal) for readability.

Suggested change
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name). "

Copilot uses AI. Check for mistakes.
f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
)
add_args_flwr_app_common(parser=parser)
return parser
129 changes: 129 additions & 0 deletions framework/py/flwr/supercore/cli/flwr_connector_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for ConnectorApp process CLI parsing and wiring."""


import importlib
from types import SimpleNamespace
from unittest.mock import Mock, patch

import pytest

from flwr.common.constant import SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS

from .flwr_connector import _parse_args_run_flwr_connector

flwr_connector_module = importlib.import_module("flwr.supercore.cli.flwr_connector")


def test_parse_flwr_connector_requires_token() -> None:
"""The ConnectorApp process CLI should require a token."""
with pytest.raises(SystemExit):
_parse_args_run_flwr_connector().parse_args([])


def test_parse_flwr_connector_rejects_run_once() -> None:
"""The removed deprecated flag should no longer parse."""
with pytest.raises(SystemExit):
_parse_args_run_flwr_connector().parse_args(
["--token", "test-token", "--run-once"]
)


def test_parse_flwr_connector_parses_tokenized_invocation() -> None:
"""The ConnectorApp process CLI should still parse the supported flags."""
args = _parse_args_run_flwr_connector().parse_args(
[
"--token",
"test-token",
"--insecure",
"--parent-pid",
"1234",
"--allow-runtime-dependency-installation",
]
)

assert args.serverappio_api_address == SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS
assert args.token == "test-token"
assert args.insecure is True
assert args.parent_pid == 1234
assert args.runtime_dependency_install is True


def test_flwr_connector_parses_args_before_mirroring_output() -> None:
"""Argument parsing should happen before stdout/stderr redirection."""

class _Parser:
def parse_args(self) -> SimpleNamespace:
"""Raise a parser error before any side effects happen."""
raise SystemExit(2)

mirror_output_to_queue = Mock()

with (
patch.object(flwr_connector_module, "_parse_args_run_flwr_connector", _Parser),
patch.object(
flwr_connector_module,
"mirror_output_to_queue",
mirror_output_to_queue,
),
pytest.raises(SystemExit),
):
flwr_connector_module.flwr_connector()

mirror_output_to_queue.assert_not_called()


def test_flwr_connector_forwards_cli_args() -> None:
"""The ConnectorApp CLI should forward parsed args to the runtime."""
args = SimpleNamespace(
insecure=True,
serverappio_api_address="127.0.0.1:9091",
token="test-token",
parent_pid=321,
runtime_dependency_install=True,
)

class _Parser:
def parse_args(self) -> SimpleNamespace:
"""Return a fixed namespace for CLI forwarding tests."""
return args

mirror_output_to_queue = Mock()
restore_output = Mock()
run_connector = Mock()

with (
patch.object(flwr_connector_module, "_parse_args_run_flwr_connector", _Parser),
patch.object(
flwr_connector_module,
"mirror_output_to_queue",
mirror_output_to_queue,
),
patch.object(flwr_connector_module, "restore_output", restore_output),
patch.object(flwr_connector_module, "run_connector", run_connector),
):
flwr_connector_module.flwr_connector()

mirror_output_to_queue.assert_called_once()
restore_output.assert_called_once_with()
run_connector.assert_called_once()
kwargs = run_connector.call_args.kwargs
assert kwargs["serverappio_api_address"] == "127.0.0.1:9091"
assert kwargs["log_queue"] is mirror_output_to_queue.call_args.args[0]
assert kwargs["token"] == "test-token"
assert kwargs["certificates"] is None
assert kwargs["parent_pid"] == 321
assert kwargs["runtime_dependency_install"] is True
75 changes: 75 additions & 0 deletions framework/py/flwr/supercore/cli/flwr_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""`flwr-model` command."""


import argparse
from logging import DEBUG, INFO
from queue import Queue

from flwr.common.args import add_args_flwr_app_common
from flwr.common.constant import SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS
from flwr.common.exit import ExitCode, flwr_exit
from flwr.common.logger import log, mirror_output_to_queue, restore_output
from flwr.supercore.executors.run_model import run_model


def flwr_model() -> None:
"""Run process-isolated Flower ModelApp."""
args = _parse_args_run_flwr_model().parse_args()

if not args.insecure:
flwr_exit(
ExitCode.COMMON_TLS_NOT_SUPPORTED,
"`flwr-model` does not support TLS yet.",
)

# Capture stdout/stderr
log_queue: Queue[str | None] = Queue()
mirror_output_to_queue(log_queue)

log(INFO, "Start `flwr-model` process")
log(
DEBUG,
"`flwr-model` will attempt to connect to SuperLink's ServerAppIo API at %s",
args.serverappio_api_address,
)
run_model(
serverappio_api_address=args.serverappio_api_address,
log_queue=log_queue,
token=args.token,
certificates=None,
parent_pid=args.parent_pid,
runtime_dependency_install=args.runtime_dependency_install,
)

# Restore stdout/stderr
restore_output()


def _parse_args_run_flwr_model() -> argparse.ArgumentParser:
"""Parse `flwr-model` command line arguments."""
parser = argparse.ArgumentParser(
description="Run a Flower ModelApp",
)
parser.add_argument(
"--serverappio-api-address",
default=SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
type=str,
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

The help= string concatenation is missing a separating space/newline between sentences, so argparse --help will render as ...).By default, .... Consider adding an explicit space at the end of the first literal (or using a single combined literal) for readability.

Suggested change
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name)."
help="Address of SuperLink's ServerAppIo API (IPv4, IPv6, or a domain name). "

Copilot uses AI. Check for mistakes.
f"By default, it is set to {SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
)
add_args_flwr_app_common(parser=parser)
return parser
Loading
Loading