flwr 1.18.0__py3-none-any.whl → 1.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. flwr/app/__init__.py +15 -0
  2. flwr/app/error.py +68 -0
  3. flwr/app/metadata.py +223 -0
  4. flwr/cli/build.py +82 -57
  5. flwr/cli/log.py +3 -3
  6. flwr/cli/login/login.py +3 -7
  7. flwr/cli/ls.py +15 -36
  8. flwr/cli/new/templates/app/code/client.baseline.py.tpl +1 -1
  9. flwr/cli/new/templates/app/code/model.baseline.py.tpl +1 -1
  10. flwr/cli/new/templates/app/code/server.baseline.py.tpl +2 -3
  11. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +14 -17
  12. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  13. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  14. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  15. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  16. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  17. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +1 -1
  18. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  19. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
  20. flwr/cli/run/run.py +10 -18
  21. flwr/cli/stop.py +2 -2
  22. flwr/cli/utils.py +31 -5
  23. flwr/client/__init__.py +2 -2
  24. flwr/client/client_app.py +1 -1
  25. flwr/client/clientapp/__init__.py +0 -7
  26. flwr/client/grpc_adapter_client/connection.py +4 -4
  27. flwr/client/grpc_rere_client/connection.py +130 -60
  28. flwr/client/grpc_rere_client/grpc_adapter.py +34 -6
  29. flwr/client/message_handler/message_handler.py +1 -1
  30. flwr/client/mod/comms_mods.py +36 -17
  31. flwr/client/rest_client/connection.py +173 -67
  32. flwr/clientapp/__init__.py +15 -0
  33. flwr/common/__init__.py +2 -2
  34. flwr/common/auth_plugin/__init__.py +2 -0
  35. flwr/common/auth_plugin/auth_plugin.py +29 -3
  36. flwr/common/constant.py +36 -7
  37. flwr/common/event_log_plugin/event_log_plugin.py +3 -3
  38. flwr/common/exit_handlers.py +30 -0
  39. flwr/common/heartbeat.py +165 -0
  40. flwr/common/inflatable.py +290 -0
  41. flwr/common/inflatable_grpc_utils.py +99 -0
  42. flwr/common/inflatable_rest_utils.py +99 -0
  43. flwr/common/inflatable_utils.py +341 -0
  44. flwr/common/message.py +110 -242
  45. flwr/common/record/__init__.py +2 -1
  46. flwr/common/record/array.py +323 -0
  47. flwr/common/record/arrayrecord.py +103 -225
  48. flwr/common/record/configrecord.py +59 -4
  49. flwr/common/record/conversion_utils.py +1 -1
  50. flwr/common/record/metricrecord.py +55 -4
  51. flwr/common/record/recorddict.py +69 -1
  52. flwr/common/recorddict_compat.py +2 -2
  53. flwr/common/retry_invoker.py +5 -1
  54. flwr/common/serde.py +59 -183
  55. flwr/common/serde_utils.py +175 -0
  56. flwr/common/typing.py +5 -3
  57. flwr/compat/__init__.py +15 -0
  58. flwr/compat/client/__init__.py +15 -0
  59. flwr/{client → compat/client}/app.py +19 -159
  60. flwr/compat/common/__init__.py +15 -0
  61. flwr/compat/server/__init__.py +15 -0
  62. flwr/compat/server/app.py +174 -0
  63. flwr/compat/simulation/__init__.py +15 -0
  64. flwr/proto/fleet_pb2.py +32 -27
  65. flwr/proto/fleet_pb2.pyi +49 -35
  66. flwr/proto/fleet_pb2_grpc.py +117 -13
  67. flwr/proto/fleet_pb2_grpc.pyi +47 -6
  68. flwr/proto/heartbeat_pb2.py +33 -0
  69. flwr/proto/heartbeat_pb2.pyi +66 -0
  70. flwr/proto/heartbeat_pb2_grpc.py +4 -0
  71. flwr/proto/heartbeat_pb2_grpc.pyi +4 -0
  72. flwr/proto/message_pb2.py +28 -11
  73. flwr/proto/message_pb2.pyi +125 -0
  74. flwr/proto/recorddict_pb2.py +16 -28
  75. flwr/proto/recorddict_pb2.pyi +46 -64
  76. flwr/proto/run_pb2.py +24 -32
  77. flwr/proto/run_pb2.pyi +4 -52
  78. flwr/proto/serverappio_pb2.py +32 -23
  79. flwr/proto/serverappio_pb2.pyi +45 -3
  80. flwr/proto/serverappio_pb2_grpc.py +138 -34
  81. flwr/proto/serverappio_pb2_grpc.pyi +54 -13
  82. flwr/proto/simulationio_pb2.py +12 -11
  83. flwr/proto/simulationio_pb2_grpc.py +35 -0
  84. flwr/proto/simulationio_pb2_grpc.pyi +14 -0
  85. flwr/server/__init__.py +1 -1
  86. flwr/server/app.py +68 -186
  87. flwr/server/compat/app_utils.py +50 -28
  88. flwr/server/fleet_event_log_interceptor.py +2 -2
  89. flwr/server/grid/grpc_grid.py +104 -34
  90. flwr/server/grid/inmemory_grid.py +5 -4
  91. flwr/server/serverapp/app.py +18 -0
  92. flwr/server/superlink/ffs/__init__.py +2 -0
  93. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +13 -3
  94. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +101 -7
  95. flwr/server/superlink/fleet/message_handler/message_handler.py +135 -18
  96. flwr/server/superlink/fleet/rest_rere/rest_api.py +72 -11
  97. flwr/server/superlink/fleet/vce/vce_api.py +6 -3
  98. flwr/server/superlink/linkstate/in_memory_linkstate.py +138 -43
  99. flwr/server/superlink/linkstate/linkstate.py +53 -20
  100. flwr/server/superlink/linkstate/sqlite_linkstate.py +149 -55
  101. flwr/server/superlink/linkstate/utils.py +33 -29
  102. flwr/server/superlink/serverappio/serverappio_grpc.py +3 -0
  103. flwr/server/superlink/serverappio/serverappio_servicer.py +211 -57
  104. flwr/server/superlink/simulation/simulationio_servicer.py +25 -1
  105. flwr/server/superlink/utils.py +44 -2
  106. flwr/server/utils/validator.py +2 -2
  107. flwr/serverapp/__init__.py +15 -0
  108. flwr/simulation/app.py +17 -0
  109. flwr/supercore/__init__.py +15 -0
  110. flwr/supercore/object_store/__init__.py +24 -0
  111. flwr/supercore/object_store/in_memory_object_store.py +229 -0
  112. flwr/supercore/object_store/object_store.py +192 -0
  113. flwr/supercore/object_store/object_store_factory.py +44 -0
  114. flwr/superexec/deployment.py +6 -2
  115. flwr/superexec/exec_event_log_interceptor.py +4 -4
  116. flwr/superexec/exec_grpc.py +7 -3
  117. flwr/superexec/exec_servicer.py +125 -23
  118. flwr/superexec/exec_user_auth_interceptor.py +37 -8
  119. flwr/superexec/executor.py +4 -0
  120. flwr/superexec/simulation.py +7 -1
  121. flwr/superlink/__init__.py +15 -0
  122. flwr/{client/supernode → supernode}/__init__.py +0 -7
  123. flwr/{client/nodestate/nodestate.py → supernode/cli/__init__.py} +7 -14
  124. flwr/{client/supernode/app.py → supernode/cli/flower_supernode.py} +3 -12
  125. flwr/supernode/cli/flwr_clientapp.py +81 -0
  126. flwr/supernode/nodestate/in_memory_nodestate.py +190 -0
  127. flwr/supernode/nodestate/nodestate.py +212 -0
  128. flwr/supernode/runtime/__init__.py +15 -0
  129. flwr/{client/clientapp/app.py → supernode/runtime/run_clientapp.py} +25 -56
  130. flwr/supernode/servicer/__init__.py +15 -0
  131. flwr/supernode/servicer/clientappio/__init__.py +24 -0
  132. flwr/supernode/start_client_internal.py +491 -0
  133. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/METADATA +5 -4
  134. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/RECORD +141 -108
  135. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/WHEEL +1 -1
  136. {flwr-1.18.0.dist-info → flwr-1.19.0.dist-info}/entry_points.txt +2 -2
  137. flwr/client/heartbeat.py +0 -74
  138. flwr/client/nodestate/in_memory_nodestate.py +0 -38
  139. /flwr/{client → compat/client}/grpc_client/__init__.py +0 -0
  140. /flwr/{client → compat/client}/grpc_client/connection.py +0 -0
  141. /flwr/{client → supernode}/nodestate/__init__.py +0 -0
  142. /flwr/{client → supernode}/nodestate/nodestate_factory.py +0 -0
  143. /flwr/{client/clientapp → supernode/servicer/clientappio}/clientappio_servicer.py +0 -0
@@ -18,21 +18,25 @@
18
18
  import time
19
19
  from collections.abc import Generator
20
20
  from logging import ERROR, INFO
21
- from typing import Any, Optional
22
- from uuid import UUID
21
+ from typing import Any, Optional, cast
23
22
 
24
23
  import grpc
25
24
 
26
25
  from flwr.common import now
27
26
  from flwr.common.auth_plugin import ExecAuthPlugin
28
- from flwr.common.constant import LOG_STREAM_INTERVAL, Status, SubStatus
27
+ from flwr.common.constant import (
28
+ LOG_STREAM_INTERVAL,
29
+ RUN_ID_NOT_FOUND_MESSAGE,
30
+ Status,
31
+ SubStatus,
32
+ )
29
33
  from flwr.common.logger import log
30
34
  from flwr.common.serde import (
31
35
  config_record_from_proto,
32
36
  run_to_proto,
33
37
  user_config_from_proto,
34
38
  )
35
- from flwr.common.typing import RunStatus
39
+ from flwr.common.typing import Run, RunStatus
36
40
  from flwr.proto import exec_pb2_grpc # pylint: disable=E0611
37
41
  from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
38
42
  GetAuthTokensRequest,
@@ -50,22 +54,26 @@ from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
50
54
  )
51
55
  from flwr.server.superlink.ffs.ffs_factory import FfsFactory
52
56
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
57
+ from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
53
58
 
59
+ from .exec_user_auth_interceptor import shared_account_info
54
60
  from .executor import Executor
55
61
 
56
62
 
57
63
  class ExecServicer(exec_pb2_grpc.ExecServicer):
58
64
  """SuperExec API servicer."""
59
65
 
60
- def __init__(
66
+ def __init__( # pylint: disable=R0913, R0917
61
67
  self,
62
68
  linkstate_factory: LinkStateFactory,
63
69
  ffs_factory: FfsFactory,
70
+ objectstore_factory: ObjectStoreFactory,
64
71
  executor: Executor,
65
72
  auth_plugin: Optional[ExecAuthPlugin] = None,
66
73
  ) -> None:
67
74
  self.linkstate_factory = linkstate_factory
68
75
  self.ffs_factory = ffs_factory
76
+ self.objectstore_factory = objectstore_factory
69
77
  self.executor = executor
70
78
  self.executor.initialize(linkstate_factory, ffs_factory)
71
79
  self.auth_plugin = auth_plugin
@@ -76,10 +84,12 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
76
84
  """Create run ID."""
77
85
  log(INFO, "ExecServicer.StartRun")
78
86
 
87
+ flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
79
88
  run_id = self.executor.start_run(
80
89
  request.fab.content,
81
90
  user_config_from_proto(request.override_config),
82
91
  config_record_from_proto(request.federation_options),
92
+ flwr_aid,
83
93
  )
84
94
 
85
95
  if run_id is None:
@@ -95,12 +105,20 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
95
105
  log(INFO, "ExecServicer.StreamLogs")
96
106
  state = self.linkstate_factory.state()
97
107
 
98
- # Retrieve run ID
108
+ # Retrieve run ID and run
99
109
  run_id = request.run_id
110
+ run = state.get_run(run_id)
100
111
 
101
112
  # Exit if `run_id` not found
102
- if not state.get_run(run_id):
103
- context.abort(grpc.StatusCode.NOT_FOUND, "Run ID not found")
113
+ if not run:
114
+ context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
115
+
116
+ # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
117
+ if self.auth_plugin:
118
+ flwr_aid = shared_account_info.get().flwr_aid
119
+ _check_flwr_aid_in_run(
120
+ flwr_aid=flwr_aid, run=cast(Run, run), context=context
121
+ )
104
122
 
105
123
  after_timestamp = request.after_timestamp + 1e-6
106
124
  while context.is_active():
@@ -119,7 +137,10 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
119
137
  # is returned at this point and the server ends the stream.
120
138
  run_status = state.get_run_status({run_id})[run_id]
121
139
  if run_status.status == Status.FINISHED:
122
- log(INFO, "All logs for run ID `%s` returned", request.run_id)
140
+ log(INFO, "All logs for run ID `%s` returned", run_id)
141
+
142
+ # Delete objects of the run from the object store
143
+ self.objectstore_factory.store().delete_objects_in_run(run_id)
123
144
  break
124
145
 
125
146
  time.sleep(LOG_STREAM_INTERVAL) # Sleep briefly to avoid busy waiting
@@ -131,11 +152,44 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
131
152
  log(INFO, "ExecServicer.List")
132
153
  state = self.linkstate_factory.state()
133
154
 
134
- # Handle `flwr ls --runs`
155
+ # Build a set of run IDs for `flwr ls --runs`
135
156
  if not request.HasField("run_id"):
136
- return _create_list_runs_response(state.get_run_ids(), state)
137
- # Handle `flwr ls --run-id <run_id>`
138
- return _create_list_runs_response({request.run_id}, state)
157
+ if self.auth_plugin:
158
+ # If no `run_id` is specified and user auth is enabled,
159
+ # return run IDs for the authenticated user
160
+ flwr_aid = shared_account_info.get().flwr_aid
161
+ if flwr_aid is None:
162
+ context.abort(
163
+ grpc.StatusCode.PERMISSION_DENIED,
164
+ "️⛔️ User authentication is enabled, but `flwr_aid` is None",
165
+ )
166
+ run_ids = state.get_run_ids(flwr_aid=flwr_aid)
167
+ else:
168
+ # If no `run_id` is specified and no user auth is enabled,
169
+ # return all run IDs
170
+ run_ids = state.get_run_ids(None)
171
+ # Build a set of run IDs for `flwr ls --run-id <run_id>`
172
+ else:
173
+ # Retrieve run ID and run
174
+ run_id = request.run_id
175
+ run = state.get_run(run_id)
176
+
177
+ # Exit if `run_id` not found
178
+ if not run:
179
+ context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
180
+
181
+ # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
182
+ if self.auth_plugin:
183
+ flwr_aid = shared_account_info.get().flwr_aid
184
+ _check_flwr_aid_in_run(
185
+ flwr_aid=flwr_aid, run=cast(Run, run), context=context
186
+ )
187
+
188
+ run_ids = {run_id}
189
+
190
+ # Init the object store
191
+ store = self.objectstore_factory.store()
192
+ return _create_list_runs_response(run_ids, state, store)
139
193
 
140
194
  def StopRun(
141
195
  self, request: StopRunRequest, context: grpc.ServicerContext
@@ -144,30 +198,42 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
144
198
  log(INFO, "ExecServicer.StopRun")
145
199
  state = self.linkstate_factory.state()
146
200
 
201
+ # Retrieve run ID and run
202
+ run_id = request.run_id
203
+ run = state.get_run(run_id)
204
+
147
205
  # Exit if `run_id` not found
148
- if not state.get_run(request.run_id):
149
- context.abort(
150
- grpc.StatusCode.NOT_FOUND, f"Run ID {request.run_id} not found"
206
+ if not run:
207
+ context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
208
+
209
+ # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
210
+ if self.auth_plugin:
211
+ flwr_aid = shared_account_info.get().flwr_aid
212
+ _check_flwr_aid_in_run(
213
+ flwr_aid=flwr_aid, run=cast(Run, run), context=context
151
214
  )
152
215
 
153
- run_status = state.get_run_status({request.run_id})[request.run_id]
216
+ run_status = state.get_run_status({run_id})[run_id]
154
217
  if run_status.status == Status.FINISHED:
155
218
  context.abort(
156
219
  grpc.StatusCode.FAILED_PRECONDITION,
157
- f"Run ID {request.run_id} is already finished",
220
+ f"Run ID {run_id} is already finished",
158
221
  )
159
222
 
160
223
  update_success = state.update_run_status(
161
- run_id=request.run_id,
224
+ run_id=run_id,
162
225
  new_status=RunStatus(Status.FINISHED, SubStatus.STOPPED, ""),
163
226
  )
164
227
 
165
228
  if update_success:
166
- message_ids: set[UUID] = state.get_message_ids_from_run_id(request.run_id)
229
+ message_ids: set[str] = state.get_message_ids_from_run_id(run_id)
167
230
 
168
231
  # Delete Messages and their replies for the `run_id`
169
232
  state.delete_messages(message_ids)
170
233
 
234
+ # Delete objects of the run from the object store
235
+ self.objectstore_factory.store().delete_objects_in_run(run_id)
236
+
171
237
  return StopRunResponse(success=update_success)
172
238
 
173
239
  def GetLoginDetails(
@@ -222,10 +288,46 @@ class ExecServicer(exec_pb2_grpc.ExecServicer):
222
288
  )
223
289
 
224
290
 
225
- def _create_list_runs_response(run_ids: set[int], state: LinkState) -> ListRunsResponse:
291
+ def _create_list_runs_response(
292
+ run_ids: set[int], state: LinkState, store: ObjectStore
293
+ ) -> ListRunsResponse:
226
294
  """Create response for `flwr ls --runs` and `flwr ls --run-id <run_id>`."""
227
- run_dict = {run_id: state.get_run(run_id) for run_id in run_ids}
295
+ run_dict = {run_id: run for run_id in run_ids if (run := state.get_run(run_id))}
296
+
297
+ # Delete objects of finished runs from the object store
298
+ for run_id, run in run_dict.items():
299
+ if run.status.status == Status.FINISHED:
300
+ store.delete_objects_in_run(run_id)
301
+
228
302
  return ListRunsResponse(
229
- run_dict={run_id: run_to_proto(run) for run_id, run in run_dict.items() if run},
303
+ run_dict={run_id: run_to_proto(run) for run_id, run in run_dict.items()},
230
304
  now=now().isoformat(),
231
305
  )
306
+
307
+
308
+ def _check_flwr_aid_in_run(
309
+ flwr_aid: Optional[str], run: Run, context: grpc.ServicerContext
310
+ ) -> None:
311
+ """Guard clause to check if `flwr_aid` matches the run's `flwr_aid`."""
312
+ # `flwr_aid` must not be None. Abort if it is None.
313
+ if flwr_aid is None:
314
+ context.abort(
315
+ grpc.StatusCode.PERMISSION_DENIED,
316
+ "️⛔️ User authentication is enabled, but `flwr_aid` is None",
317
+ )
318
+
319
+ # `run.flwr_aid` must not be an empty string. Abort if it is empty.
320
+ run_flwr_aid = run.flwr_aid
321
+ if not run_flwr_aid:
322
+ context.abort(
323
+ grpc.StatusCode.PERMISSION_DENIED,
324
+ "⛔️ User authentication is enabled, but the run is not associated "
325
+ "with a `flwr_aid`.",
326
+ )
327
+
328
+ # Exit if `flwr_aid` does not match the run's `flwr_aid`
329
+ if run_flwr_aid != flwr_aid:
330
+ context.abort(
331
+ grpc.StatusCode.PERMISSION_DENIED,
332
+ "⛔️ Run ID does not belong to the user",
333
+ )
@@ -16,12 +16,12 @@
16
16
 
17
17
 
18
18
  import contextvars
19
- from typing import Any, Callable, Union, cast
19
+ from typing import Any, Callable, Union
20
20
 
21
21
  import grpc
22
22
 
23
- from flwr.common.auth_plugin import ExecAuthPlugin
24
- from flwr.common.typing import UserInfo
23
+ from flwr.common.auth_plugin import ExecAuthPlugin, ExecAuthzPlugin
24
+ from flwr.common.typing import AccountInfo
25
25
  from flwr.proto.exec_pb2 import ( # pylint: disable=E0611
26
26
  GetAuthTokensRequest,
27
27
  GetAuthTokensResponse,
@@ -45,8 +45,8 @@ Response = Union[
45
45
  ]
46
46
 
47
47
 
48
- shared_user_info: contextvars.ContextVar[UserInfo] = contextvars.ContextVar(
49
- "user_info", default=UserInfo(user_id=None, user_name=None)
48
+ shared_account_info: contextvars.ContextVar[AccountInfo] = contextvars.ContextVar(
49
+ "account_info", default=AccountInfo(flwr_aid=None, account_name=None)
50
50
  )
51
51
 
52
52
 
@@ -56,8 +56,10 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
56
56
  def __init__(
57
57
  self,
58
58
  auth_plugin: ExecAuthPlugin,
59
+ authz_plugin: ExecAuthzPlugin,
59
60
  ):
60
61
  self.auth_plugin = auth_plugin
62
+ self.authz_plugin = authz_plugin
61
63
 
62
64
  def intercept_service(
63
65
  self,
@@ -91,17 +93,44 @@ class ExecUserAuthInterceptor(grpc.ServerInterceptor): # type: ignore
91
93
  return call(request, context) # type: ignore
92
94
 
93
95
  # For other requests, check if the user is authenticated
94
- valid_tokens, user_info = self.auth_plugin.validate_tokens_in_metadata(
96
+ valid_tokens, account_info = self.auth_plugin.validate_tokens_in_metadata(
95
97
  metadata
96
98
  )
97
99
  if valid_tokens:
100
+ if account_info is None:
101
+ context.abort(
102
+ grpc.StatusCode.UNAUTHENTICATED,
103
+ "Tokens validated, but user info not found",
104
+ )
105
+ raise grpc.RpcError()
98
106
  # Store user info in contextvars for authenticated users
99
- shared_user_info.set(cast(UserInfo, user_info))
107
+ shared_account_info.set(account_info)
108
+ # Check if the user is authorized
109
+ if not self.authz_plugin.verify_user_authorization(account_info):
110
+ context.abort(
111
+ grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
112
+ )
113
+ raise grpc.RpcError()
100
114
  return call(request, context) # type: ignore
101
115
 
102
116
  # If the user is not authenticated, refresh tokens
103
- tokens = self.auth_plugin.refresh_tokens(context.invocation_metadata())
117
+ tokens, account_info = self.auth_plugin.refresh_tokens(metadata)
104
118
  if tokens is not None:
119
+ if account_info is None:
120
+ context.abort(
121
+ grpc.StatusCode.UNAUTHENTICATED,
122
+ "Tokens refreshed, but user info not found",
123
+ )
124
+ raise grpc.RpcError()
125
+ # Store user info in contextvars for authenticated users
126
+ shared_account_info.set(account_info)
127
+ # Check if the user is authorized
128
+ if not self.authz_plugin.verify_user_authorization(account_info):
129
+ context.abort(
130
+ grpc.StatusCode.PERMISSION_DENIED, "User not authorized"
131
+ )
132
+ raise grpc.RpcError()
133
+
105
134
  context.send_initial_metadata(tokens)
106
135
  return call(request, context) # type: ignore
107
136
 
@@ -74,6 +74,7 @@ class Executor(ABC):
74
74
  fab_file: bytes,
75
75
  override_config: UserConfig,
76
76
  federation_options: ConfigRecord,
77
+ flwr_aid: Optional[str],
77
78
  ) -> Optional[int]:
78
79
  """Start a run using the given Flower FAB ID and version.
79
80
 
@@ -88,6 +89,9 @@ class Executor(ABC):
88
89
  The config overrides dict sent by the user (using `flwr run`).
89
90
  federation_options: ConfigRecord
90
91
  The federation options sent by the user (using `flwr run`).
92
+ flwr_aid : Optional[str]
93
+ The Flower Account ID of the user starting the run, if authentication is
94
+ enabled.
91
95
 
92
96
  Returns
93
97
  -------
@@ -77,6 +77,7 @@ class SimulationEngine(Executor):
77
77
  fab_file: bytes,
78
78
  override_config: UserConfig,
79
79
  federation_options: ConfigRecord,
80
+ flwr_aid: Optional[str],
80
81
  ) -> Optional[int]:
81
82
  """Start run using the Flower Simulation Engine."""
82
83
  try:
@@ -96,7 +97,12 @@ class SimulationEngine(Executor):
96
97
  fab_id, fab_version = get_fab_metadata(fab.content)
97
98
 
98
99
  run_id = self.linkstate.create_run(
99
- fab_id, fab_version, fab_hash, override_config, federation_options
100
+ fab_id,
101
+ fab_version,
102
+ fab_hash,
103
+ override_config,
104
+ federation_options,
105
+ flwr_aid,
100
106
  )
101
107
 
102
108
  # Create an empty context for the Run
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Flower SuperLink."""
@@ -13,10 +13,3 @@
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
15
  """Flower SuperNode."""
16
-
17
-
18
- from .app import run_supernode as run_supernode
19
-
20
- __all__ = [
21
- "run_supernode",
22
- ]
@@ -12,20 +12,13 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Abstract base class NodeState."""
15
+ """Flower command line interface for SuperNode."""
16
16
 
17
17
 
18
- import abc
19
- from typing import Optional
18
+ from .flower_supernode import flower_supernode
19
+ from .flwr_clientapp import flwr_clientapp
20
20
 
21
-
22
- class NodeState(abc.ABC):
23
- """Abstract NodeState."""
24
-
25
- @abc.abstractmethod
26
- def set_node_id(self, node_id: Optional[int]) -> None:
27
- """Set the node ID."""
28
-
29
- @abc.abstractmethod
30
- def get_node_id(self) -> int:
31
- """Get the node ID."""
21
+ __all__ = [
22
+ "flower_supernode",
23
+ "flwr_clientapp",
24
+ ]
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Flower SuperNode."""
15
+ """`flower-supernode` command."""
16
16
 
17
17
 
18
18
  import argparse
@@ -42,12 +42,10 @@ from flwr.common.constant import (
42
42
  from flwr.common.exit import ExitCode, flwr_exit
43
43
  from flwr.common.exit_handlers import register_exit_handlers
44
44
  from flwr.common.logger import log
45
+ from flwr.supernode.start_client_internal import start_client_internal
45
46
 
46
- from ..app import start_client_internal
47
- from ..clientapp.utils import get_load_client_app_fn
48
47
 
49
-
50
- def run_supernode() -> None:
48
+ def flower_supernode() -> None:
51
49
  """Run Flower SuperNode."""
52
50
  args = _parse_args_run_supernode().parse_args()
53
51
 
@@ -64,12 +62,6 @@ def run_supernode() -> None:
64
62
  )
65
63
 
66
64
  root_certificates = try_obtain_root_certificates(args, args.superlink)
67
- load_fn = get_load_client_app_fn(
68
- default_app_ref="",
69
- app_path=None,
70
- flwr_dir=args.flwr_dir,
71
- multi_app=True,
72
- )
73
65
  authentication_keys = _try_setup_client_authentication(args)
74
66
 
75
67
  log(DEBUG, "Isolation mode: %s", args.isolation)
@@ -82,7 +74,6 @@ def run_supernode() -> None:
82
74
 
83
75
  start_client_internal(
84
76
  server_address=args.superlink,
85
- load_client_app_fn=load_fn,
86
77
  transport=args.transport,
87
78
  root_certificates=root_certificates,
88
79
  insecure=args.insecure,
@@ -0,0 +1,81 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """`flwr-clientapp` command."""
16
+
17
+
18
+ import argparse
19
+ from logging import DEBUG, INFO
20
+
21
+ from flwr.common.args import add_args_flwr_app_common
22
+ from flwr.common.constant import CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS
23
+ from flwr.common.exit import ExitCode, flwr_exit
24
+ from flwr.common.logger import log
25
+ from flwr.supernode.runtime.run_clientapp import run_clientapp
26
+
27
+
28
+ def flwr_clientapp() -> None:
29
+ """Run process-isolated Flower ClientApp."""
30
+ args = _parse_args_run_flwr_clientapp().parse_args()
31
+ if not args.insecure:
32
+ flwr_exit(
33
+ ExitCode.COMMON_TLS_NOT_SUPPORTED,
34
+ "flwr-clientapp does not support TLS yet.",
35
+ )
36
+
37
+ log(INFO, "Start `flwr-clientapp` process")
38
+ log(
39
+ DEBUG,
40
+ "`flwr-clientapp` will attempt to connect to SuperNode's "
41
+ "ClientAppIo API at %s with token %s",
42
+ args.clientappio_api_address,
43
+ args.token,
44
+ )
45
+ run_clientapp(
46
+ clientappio_api_address=args.clientappio_api_address,
47
+ run_once=(args.token is not None),
48
+ token=args.token,
49
+ flwr_dir=args.flwr_dir,
50
+ certificates=None,
51
+ parent_pid=args.parent_pid,
52
+ )
53
+
54
+
55
+ def _parse_args_run_flwr_clientapp() -> argparse.ArgumentParser:
56
+ """Parse flwr-clientapp command line arguments."""
57
+ parser = argparse.ArgumentParser(
58
+ description="Run a Flower ClientApp",
59
+ )
60
+ parser.add_argument(
61
+ "--clientappio-api-address",
62
+ default=CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS,
63
+ type=str,
64
+ help="Address of SuperNode's ClientAppIo API (IPv4, IPv6, or a domain name)."
65
+ f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_CLIENT_ADDRESS}.",
66
+ )
67
+ parser.add_argument(
68
+ "--token",
69
+ type=int,
70
+ required=False,
71
+ help="Unique token generated by SuperNode for each ClientApp execution",
72
+ )
73
+ parser.add_argument(
74
+ "--parent-pid",
75
+ type=int,
76
+ default=None,
77
+ help="The PID of the parent process. When set, the process will terminate "
78
+ "when the parent process exits.",
79
+ )
80
+ add_args_flwr_app_common(parser=parser)
81
+ return parser