flwr 1.21.0__py3-none-any.whl → 1.23.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.
- flwr/cli/app.py +17 -1
- flwr/cli/auth_plugin/__init__.py +15 -6
- flwr/cli/auth_plugin/auth_plugin.py +95 -0
- flwr/cli/auth_plugin/noop_auth_plugin.py +58 -0
- flwr/cli/auth_plugin/oidc_cli_plugin.py +16 -25
- flwr/cli/build.py +118 -47
- flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +6 -5
- flwr/cli/log.py +2 -2
- flwr/cli/login/login.py +34 -23
- flwr/cli/ls.py +13 -9
- flwr/cli/new/new.py +196 -42
- flwr/cli/new/templates/app/README.flowertune.md.tpl +1 -1
- flwr/cli/new/templates/app/code/client.baseline.py.tpl +64 -47
- flwr/cli/new/templates/app/code/client.huggingface.py.tpl +68 -30
- flwr/cli/new/templates/app/code/client.jax.py.tpl +63 -42
- flwr/cli/new/templates/app/code/client.mlx.py.tpl +80 -51
- flwr/cli/new/templates/app/code/client.numpy.py.tpl +36 -13
- flwr/cli/new/templates/app/code/client.pytorch.py.tpl +71 -46
- flwr/cli/new/templates/app/code/client.pytorch_legacy_api.py.tpl +55 -0
- flwr/cli/new/templates/app/code/client.sklearn.py.tpl +75 -30
- flwr/cli/new/templates/app/code/client.tensorflow.py.tpl +69 -44
- flwr/cli/new/templates/app/code/client.xgboost.py.tpl +110 -0
- flwr/cli/new/templates/app/code/flwr_tune/client_app.py.tpl +56 -90
- flwr/cli/new/templates/app/code/flwr_tune/models.py.tpl +1 -23
- flwr/cli/new/templates/app/code/flwr_tune/server_app.py.tpl +37 -58
- flwr/cli/new/templates/app/code/flwr_tune/strategy.py.tpl +39 -44
- flwr/cli/new/templates/app/code/model.baseline.py.tpl +0 -14
- flwr/cli/new/templates/app/code/server.baseline.py.tpl +27 -29
- flwr/cli/new/templates/app/code/server.huggingface.py.tpl +23 -19
- flwr/cli/new/templates/app/code/server.jax.py.tpl +27 -14
- flwr/cli/new/templates/app/code/server.mlx.py.tpl +29 -19
- flwr/cli/new/templates/app/code/server.numpy.py.tpl +30 -17
- flwr/cli/new/templates/app/code/server.pytorch.py.tpl +36 -26
- flwr/cli/new/templates/app/code/server.pytorch_legacy_api.py.tpl +31 -0
- flwr/cli/new/templates/app/code/server.sklearn.py.tpl +29 -21
- flwr/cli/new/templates/app/code/server.tensorflow.py.tpl +28 -19
- flwr/cli/new/templates/app/code/server.xgboost.py.tpl +56 -0
- flwr/cli/new/templates/app/code/task.huggingface.py.tpl +16 -20
- flwr/cli/new/templates/app/code/task.jax.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.numpy.py.tpl +1 -1
- flwr/cli/new/templates/app/code/task.pytorch.py.tpl +14 -27
- flwr/cli/new/templates/app/code/{task.pytorch_msg_api.py.tpl → task.pytorch_legacy_api.py.tpl} +27 -14
- flwr/cli/new/templates/app/code/task.tensorflow.py.tpl +1 -2
- flwr/cli/new/templates/app/code/task.xgboost.py.tpl +67 -0
- flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +4 -4
- flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +2 -2
- flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
- flwr/cli/new/templates/app/{pyproject.pytorch_msg_api.toml.tpl → pyproject.pytorch_legacy_api.toml.tpl} +3 -3
- flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +1 -1
- flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +61 -0
- flwr/cli/pull.py +100 -0
- flwr/cli/run/run.py +11 -7
- flwr/cli/stop.py +2 -2
- flwr/cli/supernode/__init__.py +25 -0
- flwr/cli/supernode/ls.py +260 -0
- flwr/cli/supernode/register.py +185 -0
- flwr/cli/supernode/unregister.py +138 -0
- flwr/cli/utils.py +109 -69
- flwr/client/__init__.py +2 -1
- flwr/client/grpc_adapter_client/connection.py +6 -8
- flwr/client/grpc_rere_client/connection.py +59 -31
- flwr/client/grpc_rere_client/grpc_adapter.py +28 -12
- flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +3 -6
- flwr/client/mod/secure_aggregation/secaggplus_mod.py +7 -5
- flwr/client/rest_client/connection.py +82 -37
- flwr/clientapp/__init__.py +1 -2
- flwr/clientapp/mod/__init__.py +4 -1
- flwr/clientapp/mod/centraldp_mods.py +156 -40
- flwr/clientapp/mod/localdp_mod.py +169 -0
- flwr/clientapp/typing.py +22 -0
- flwr/{client/clientapp → clientapp}/utils.py +1 -1
- flwr/common/constant.py +56 -13
- flwr/common/exit/exit_code.py +24 -10
- flwr/common/inflatable_utils.py +10 -10
- flwr/common/record/array.py +3 -3
- flwr/common/record/arrayrecord.py +10 -1
- flwr/common/record/typeddict.py +12 -0
- flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
- flwr/common/serde.py +4 -2
- flwr/common/typing.py +7 -6
- flwr/compat/client/app.py +1 -1
- flwr/compat/client/grpc_client/connection.py +2 -2
- flwr/proto/control_pb2.py +48 -31
- flwr/proto/control_pb2.pyi +95 -5
- flwr/proto/control_pb2_grpc.py +136 -0
- flwr/proto/control_pb2_grpc.pyi +52 -0
- flwr/proto/fab_pb2.py +11 -7
- flwr/proto/fab_pb2.pyi +21 -1
- flwr/proto/fleet_pb2.py +31 -23
- flwr/proto/fleet_pb2.pyi +63 -23
- flwr/proto/fleet_pb2_grpc.py +98 -28
- flwr/proto/fleet_pb2_grpc.pyi +45 -13
- flwr/proto/node_pb2.py +3 -1
- flwr/proto/node_pb2.pyi +48 -0
- flwr/server/app.py +152 -114
- flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +17 -7
- flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +132 -38
- flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +27 -51
- flwr/server/superlink/fleet/message_handler/message_handler.py +67 -22
- flwr/server/superlink/fleet/rest_rere/rest_api.py +52 -31
- flwr/server/superlink/fleet/vce/backend/backend.py +1 -1
- flwr/server/superlink/fleet/vce/backend/raybackend.py +1 -1
- flwr/server/superlink/fleet/vce/vce_api.py +18 -5
- flwr/server/superlink/linkstate/in_memory_linkstate.py +167 -73
- flwr/server/superlink/linkstate/linkstate.py +107 -24
- flwr/server/superlink/linkstate/linkstate_factory.py +2 -1
- flwr/server/superlink/linkstate/sqlite_linkstate.py +306 -255
- flwr/server/superlink/linkstate/utils.py +3 -54
- flwr/server/superlink/serverappio/serverappio_servicer.py +2 -2
- flwr/server/superlink/simulation/simulationio_servicer.py +1 -1
- flwr/server/utils/validator.py +2 -3
- flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +4 -2
- flwr/serverapp/strategy/__init__.py +26 -0
- flwr/serverapp/strategy/bulyan.py +238 -0
- flwr/serverapp/strategy/dp_adaptive_clipping.py +335 -0
- flwr/serverapp/strategy/dp_fixed_clipping.py +71 -49
- flwr/serverapp/strategy/fedadagrad.py +0 -3
- flwr/serverapp/strategy/fedadam.py +0 -3
- flwr/serverapp/strategy/fedavg.py +89 -64
- flwr/serverapp/strategy/fedavgm.py +198 -0
- flwr/serverapp/strategy/fedmedian.py +105 -0
- flwr/serverapp/strategy/fedprox.py +174 -0
- flwr/serverapp/strategy/fedtrimmedavg.py +176 -0
- flwr/serverapp/strategy/fedxgb_bagging.py +117 -0
- flwr/serverapp/strategy/fedxgb_cyclic.py +220 -0
- flwr/serverapp/strategy/fedyogi.py +0 -3
- flwr/serverapp/strategy/krum.py +112 -0
- flwr/serverapp/strategy/multikrum.py +247 -0
- flwr/serverapp/strategy/qfedavg.py +252 -0
- flwr/serverapp/strategy/strategy_utils.py +48 -0
- flwr/simulation/app.py +1 -1
- flwr/simulation/ray_transport/ray_actor.py +1 -1
- flwr/simulation/ray_transport/ray_client_proxy.py +1 -1
- flwr/simulation/run_simulation.py +28 -32
- flwr/supercore/cli/flower_superexec.py +26 -1
- flwr/supercore/constant.py +41 -0
- flwr/supercore/object_store/in_memory_object_store.py +0 -4
- flwr/supercore/object_store/object_store_factory.py +26 -6
- flwr/supercore/object_store/sqlite_object_store.py +252 -0
- flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
- flwr/supercore/primitives/asymmetric.py +117 -0
- flwr/supercore/primitives/asymmetric_ed25519.py +165 -0
- flwr/supercore/sqlite_mixin.py +156 -0
- flwr/supercore/superexec/plugin/exec_plugin.py +11 -1
- flwr/supercore/superexec/run_superexec.py +16 -2
- flwr/supercore/utils.py +20 -0
- flwr/superlink/artifact_provider/__init__.py +22 -0
- flwr/superlink/artifact_provider/artifact_provider.py +37 -0
- flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
- flwr/superlink/auth_plugin/auth_plugin.py +91 -0
- flwr/superlink/auth_plugin/noop_auth_plugin.py +87 -0
- flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +19 -19
- flwr/superlink/servicer/control/control_event_log_interceptor.py +1 -1
- flwr/superlink/servicer/control/control_grpc.py +16 -11
- flwr/superlink/servicer/control/control_servicer.py +207 -58
- flwr/supernode/cli/flower_supernode.py +19 -26
- flwr/supernode/runtime/run_clientapp.py +2 -2
- flwr/supernode/servicer/clientappio/clientappio_servicer.py +1 -1
- flwr/supernode/start_client_internal.py +17 -9
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/METADATA +6 -16
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/RECORD +170 -140
- flwr/cli/new/templates/app/code/client.pytorch_msg_api.py.tpl +0 -80
- flwr/cli/new/templates/app/code/server.pytorch_msg_api.py.tpl +0 -41
- flwr/common/auth_plugin/auth_plugin.py +0 -149
- flwr/serverapp/dp_fixed_clipping.py +0 -352
- flwr/serverapp/strategy/strategy_utils_tests.py +0 -304
- /flwr/cli/new/templates/app/code/{__init__.pytorch_msg_api.py.tpl → __init__.pytorch_legacy_api.py.tpl} +0 -0
- /flwr/{client → clientapp}/client_app.py +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/WHEEL +0 -0
- {flwr-1.21.0.dist-info → flwr-1.23.0.dist-info}/entry_points.txt +0 -0
|
@@ -16,13 +16,26 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
from collections import OrderedDict
|
|
19
|
-
from logging import
|
|
19
|
+
from logging import ERROR, INFO
|
|
20
20
|
from typing import cast
|
|
21
21
|
|
|
22
|
-
from flwr.
|
|
23
|
-
from flwr.
|
|
24
|
-
from flwr.common
|
|
25
|
-
|
|
22
|
+
from flwr.app import Error
|
|
23
|
+
from flwr.clientapp.typing import ClientAppCallable
|
|
24
|
+
from flwr.common import (
|
|
25
|
+
Array,
|
|
26
|
+
ArrayRecord,
|
|
27
|
+
ConfigRecord,
|
|
28
|
+
Context,
|
|
29
|
+
Message,
|
|
30
|
+
MetricRecord,
|
|
31
|
+
log,
|
|
32
|
+
)
|
|
33
|
+
from flwr.common.constant import ErrorCode
|
|
34
|
+
from flwr.common.differential_privacy import (
|
|
35
|
+
compute_adaptive_clip_model_update,
|
|
36
|
+
compute_clip_model_update,
|
|
37
|
+
)
|
|
38
|
+
from flwr.common.differential_privacy_constants import KEY_CLIPPING_NORM, KEY_NORM_BIT
|
|
26
39
|
|
|
27
40
|
|
|
28
41
|
# pylint: disable=too-many-return-statements
|
|
@@ -46,33 +59,15 @@ def fixedclipping_mod(
|
|
|
46
59
|
|
|
47
60
|
Typically, fixedclipping_mod should be the last to operate on params.
|
|
48
61
|
"""
|
|
49
|
-
if msg.metadata.message_type != MessageType.TRAIN:
|
|
50
|
-
return call_next(msg, ctxt)
|
|
51
|
-
|
|
52
62
|
if len(msg.content.array_records) != 1:
|
|
53
|
-
|
|
54
|
-
WARN,
|
|
55
|
-
"fixedclipping_mod is designed to work with a single ArrayRecord. "
|
|
56
|
-
"Skipping.",
|
|
57
|
-
)
|
|
58
|
-
return call_next(msg, ctxt)
|
|
59
|
-
|
|
63
|
+
return _handle_multi_record_err("fixedclipping_mod", msg, ArrayRecord)
|
|
60
64
|
if len(msg.content.config_records) != 1:
|
|
61
|
-
|
|
62
|
-
WARN,
|
|
63
|
-
"fixedclipping_mod is designed to work with a single ConfigRecord. "
|
|
64
|
-
"Skipping.",
|
|
65
|
-
)
|
|
66
|
-
return call_next(msg, ctxt)
|
|
65
|
+
return _handle_multi_record_err("fixedclipping_mod", msg, ConfigRecord)
|
|
67
66
|
|
|
68
67
|
# Get keys in the single ConfigRecord
|
|
69
68
|
keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
|
|
70
69
|
if KEY_CLIPPING_NORM not in keys_in_config:
|
|
71
|
-
|
|
72
|
-
f"The {KEY_CLIPPING_NORM} value is not supplied by the "
|
|
73
|
-
f"`DifferentialPrivacyClientSideFixedClipping` wrapper at"
|
|
74
|
-
f" the server side."
|
|
75
|
-
)
|
|
70
|
+
return _handle_no_key_err("fixedclipping_mod", msg)
|
|
76
71
|
# Record array record communicated to client and clipping norm
|
|
77
72
|
original_array_record = next(iter(msg.content.array_records.values()))
|
|
78
73
|
clipping_norm = cast(
|
|
@@ -86,26 +81,16 @@ def fixedclipping_mod(
|
|
|
86
81
|
if out_msg.has_error():
|
|
87
82
|
return out_msg
|
|
88
83
|
|
|
89
|
-
# Ensure
|
|
84
|
+
# Ensure reply has a single ArrayRecord
|
|
90
85
|
if len(out_msg.content.array_records) != 1:
|
|
91
|
-
|
|
92
|
-
WARN,
|
|
93
|
-
"fixedclipping_mod is designed to work with a single ArrayRecord. "
|
|
94
|
-
"Skipping.",
|
|
95
|
-
)
|
|
96
|
-
return out_msg
|
|
86
|
+
return _handle_multi_record_err("fixedclipping_mod", out_msg, ArrayRecord)
|
|
97
87
|
|
|
98
88
|
new_array_record_key, client_to_server_arrecord = next(
|
|
99
89
|
iter(out_msg.content.array_records.items())
|
|
100
90
|
)
|
|
101
91
|
# Ensure keys in returned ArrayRecord match those in the one sent from server
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
WARN,
|
|
105
|
-
"fixedclipping_mod: Keys in ArrayRecord must match those from the model "
|
|
106
|
-
"that the ClientApp received. Skipping.",
|
|
107
|
-
)
|
|
108
|
-
return out_msg
|
|
92
|
+
if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
|
|
93
|
+
return _handle_array_key_mismatch_err("fixedclipping_mod", out_msg)
|
|
109
94
|
|
|
110
95
|
client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
|
|
111
96
|
# Clip the client update
|
|
@@ -130,3 +115,134 @@ def fixedclipping_mod(
|
|
|
130
115
|
)
|
|
131
116
|
)
|
|
132
117
|
return out_msg
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def adaptiveclipping_mod(
|
|
121
|
+
msg: Message, ctxt: Context, call_next: ClientAppCallable
|
|
122
|
+
) -> Message:
|
|
123
|
+
"""Client-side adaptive clipping modifier.
|
|
124
|
+
|
|
125
|
+
This mod needs to be used with the DifferentialPrivacyClientSideAdaptiveClipping
|
|
126
|
+
server-side strategy wrapper.
|
|
127
|
+
|
|
128
|
+
The wrapper sends the clipping_norm value to the client.
|
|
129
|
+
|
|
130
|
+
This mod clips the client model updates before sending them to the server.
|
|
131
|
+
|
|
132
|
+
It also sends KEY_NORM_BIT to the server for computing the new clipping value.
|
|
133
|
+
|
|
134
|
+
It operates on messages of type `MessageType.TRAIN`.
|
|
135
|
+
|
|
136
|
+
Notes
|
|
137
|
+
-----
|
|
138
|
+
Consider the order of mods when using multiple.
|
|
139
|
+
|
|
140
|
+
Typically, adaptiveclipping_mod should be the last to operate on params.
|
|
141
|
+
"""
|
|
142
|
+
if len(msg.content.array_records) != 1:
|
|
143
|
+
return _handle_multi_record_err("adaptiveclipping_mod", msg, ArrayRecord)
|
|
144
|
+
if len(msg.content.config_records) != 1:
|
|
145
|
+
return _handle_multi_record_err("adaptiveclipping_mod", msg, ConfigRecord)
|
|
146
|
+
|
|
147
|
+
# Get keys in the single ConfigRecord
|
|
148
|
+
keys_in_config = set(next(iter(msg.content.config_records.values())).keys())
|
|
149
|
+
if KEY_CLIPPING_NORM not in keys_in_config:
|
|
150
|
+
return _handle_no_key_err("adaptiveclipping_mod", msg)
|
|
151
|
+
|
|
152
|
+
# Record array record communicated to client and clipping norm
|
|
153
|
+
original_array_record = next(iter(msg.content.array_records.values()))
|
|
154
|
+
clipping_norm = cast(
|
|
155
|
+
float, next(iter(msg.content.config_records.values()))[KEY_CLIPPING_NORM]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Call inner app
|
|
159
|
+
out_msg = call_next(msg, ctxt)
|
|
160
|
+
|
|
161
|
+
# Check if the msg has error
|
|
162
|
+
if out_msg.has_error():
|
|
163
|
+
return out_msg
|
|
164
|
+
|
|
165
|
+
# Ensure reply has a single ArrayRecord
|
|
166
|
+
if len(out_msg.content.array_records) != 1:
|
|
167
|
+
return _handle_multi_record_err("adaptiveclipping_mod", out_msg, ArrayRecord)
|
|
168
|
+
|
|
169
|
+
# Ensure reply has a single MetricRecord
|
|
170
|
+
if len(out_msg.content.metric_records) != 1:
|
|
171
|
+
return _handle_multi_record_err("adaptiveclipping_mod", out_msg, MetricRecord)
|
|
172
|
+
|
|
173
|
+
new_array_record_key, client_to_server_arrecord = next(
|
|
174
|
+
iter(out_msg.content.array_records.items())
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Ensure keys in returned ArrayRecord match those in the one sent from server
|
|
178
|
+
if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
|
|
179
|
+
return _handle_array_key_mismatch_err("adaptiveclipping_mod", out_msg)
|
|
180
|
+
|
|
181
|
+
client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
|
|
182
|
+
# Clip the client update
|
|
183
|
+
norm_bit = compute_adaptive_clip_model_update(
|
|
184
|
+
client_to_server_ndarrays,
|
|
185
|
+
original_array_record.to_numpy_ndarrays(),
|
|
186
|
+
clipping_norm,
|
|
187
|
+
)
|
|
188
|
+
log(
|
|
189
|
+
INFO,
|
|
190
|
+
"adaptiveclipping_mod: ndarrays are clipped by value: %.4f.",
|
|
191
|
+
clipping_norm,
|
|
192
|
+
)
|
|
193
|
+
# Replace outgoing ArrayRecord's Array while preserving their keys
|
|
194
|
+
out_msg.content.array_records[new_array_record_key] = ArrayRecord(
|
|
195
|
+
OrderedDict(
|
|
196
|
+
{
|
|
197
|
+
k: Array(v)
|
|
198
|
+
for k, v in zip(
|
|
199
|
+
client_to_server_arrecord.keys(), client_to_server_ndarrays
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
# Add to the MetricRecords the norm bit (recall reply messages only contain
|
|
205
|
+
# one MetricRecord)
|
|
206
|
+
metric_record_key = list(out_msg.content.metric_records.keys())[0]
|
|
207
|
+
# We cast it to `int` because MetricRecord does not support `bool` values
|
|
208
|
+
out_msg.content.metric_records[metric_record_key][KEY_NORM_BIT] = int(norm_bit)
|
|
209
|
+
return out_msg
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _handle_err(msg: Message, reason: str) -> Message:
|
|
213
|
+
"""Log and return error message."""
|
|
214
|
+
log(ERROR, reason)
|
|
215
|
+
return Message(
|
|
216
|
+
Error(code=ErrorCode.MOD_FAILED_PRECONDITION, reason=reason),
|
|
217
|
+
reply_to=msg,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _handle_multi_record_err(mod_name: str, msg: Message, record_type: type) -> Message:
|
|
222
|
+
"""Log and return multi-record error."""
|
|
223
|
+
cnt = sum(isinstance(_, record_type) for _ in msg.content.values())
|
|
224
|
+
return _handle_err(
|
|
225
|
+
msg,
|
|
226
|
+
f"{mod_name} expects exactly one {record_type.__name__}, "
|
|
227
|
+
f"but found {cnt} {record_type.__name__}(s).",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _handle_no_key_err(mod_name: str, msg: Message) -> Message:
|
|
232
|
+
"""Log and return no-key error."""
|
|
233
|
+
return _handle_err(
|
|
234
|
+
msg,
|
|
235
|
+
f"{mod_name} requires the key '{KEY_CLIPPING_NORM}' to be present in the "
|
|
236
|
+
"ConfigRecord, but it was not found. "
|
|
237
|
+
"Please ensure the `DifferentialPrivacyClientSideFixedClipping` wrapper "
|
|
238
|
+
"is used in the ServerApp.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _handle_array_key_mismatch_err(mod_name: str, msg: Message) -> Message:
|
|
243
|
+
"""Create array-key-mismatch error reasons."""
|
|
244
|
+
return _handle_err(
|
|
245
|
+
msg,
|
|
246
|
+
f"{mod_name} expects the keys in the ArrayRecord of the reply message to match "
|
|
247
|
+
"those from the ArrayRecord that the ClientApp received, but they do not.",
|
|
248
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
"""Local DP modifier."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from collections import OrderedDict
|
|
19
|
+
from logging import INFO
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from flwr.clientapp.typing import ClientAppCallable
|
|
24
|
+
from flwr.common import Array, ArrayRecord
|
|
25
|
+
from flwr.common.context import Context
|
|
26
|
+
from flwr.common.differential_privacy import (
|
|
27
|
+
add_gaussian_noise_inplace,
|
|
28
|
+
compute_clip_model_update,
|
|
29
|
+
)
|
|
30
|
+
from flwr.common.logger import log
|
|
31
|
+
from flwr.common.message import Message
|
|
32
|
+
|
|
33
|
+
from .centraldp_mods import _handle_array_key_mismatch_err, _handle_multi_record_err
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LocalDpMod:
|
|
37
|
+
"""Modifier for local differential privacy.
|
|
38
|
+
|
|
39
|
+
This mod clips the client model updates and
|
|
40
|
+
adds noise to the params before sending them to the server.
|
|
41
|
+
|
|
42
|
+
It operates on messages of type `MessageType.TRAIN`.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
clipping_norm : float
|
|
47
|
+
The value of the clipping norm.
|
|
48
|
+
sensitivity : float
|
|
49
|
+
The sensitivity of the client model.
|
|
50
|
+
epsilon : float
|
|
51
|
+
The privacy budget.
|
|
52
|
+
Smaller value of epsilon indicates a higher level of privacy protection.
|
|
53
|
+
delta : float
|
|
54
|
+
The failure probability.
|
|
55
|
+
The probability that the privacy mechanism
|
|
56
|
+
fails to provide the desired level of privacy.
|
|
57
|
+
A smaller value of delta indicates a stricter privacy guarantee.
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
Create an instance of the local DP mod and add it to the client-side mods::
|
|
62
|
+
|
|
63
|
+
local_dp_mod = LocalDpMod( ... )
|
|
64
|
+
app = fl.client.ClientApp(mods=[local_dp_mod])
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self, clipping_norm: float, sensitivity: float, epsilon: float, delta: float
|
|
69
|
+
) -> None:
|
|
70
|
+
if clipping_norm <= 0:
|
|
71
|
+
raise ValueError("The clipping norm should be a positive value.")
|
|
72
|
+
|
|
73
|
+
if sensitivity < 0:
|
|
74
|
+
raise ValueError("The sensitivity should be a non-negative value.")
|
|
75
|
+
|
|
76
|
+
if epsilon < 0:
|
|
77
|
+
raise ValueError("Epsilon should be a non-negative value.")
|
|
78
|
+
|
|
79
|
+
if delta < 0:
|
|
80
|
+
raise ValueError("Delta should be a non-negative value.")
|
|
81
|
+
|
|
82
|
+
self.clipping_norm = clipping_norm
|
|
83
|
+
self.sensitivity = sensitivity
|
|
84
|
+
self.epsilon = epsilon
|
|
85
|
+
self.delta = delta
|
|
86
|
+
|
|
87
|
+
def __call__(
|
|
88
|
+
self, msg: Message, ctxt: Context, call_next: ClientAppCallable
|
|
89
|
+
) -> Message:
|
|
90
|
+
"""Perform local DP on the client model parameters.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
msg : Message
|
|
95
|
+
The message received from the ServerApp.
|
|
96
|
+
ctxt : Context
|
|
97
|
+
The context of the ClientApp.
|
|
98
|
+
call_next : ClientAppCallable
|
|
99
|
+
The callable to call the next mod (or the ClientApp) in the chain.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
Message
|
|
104
|
+
The modified message to be sent back to the server.
|
|
105
|
+
"""
|
|
106
|
+
if len(msg.content.array_records) != 1:
|
|
107
|
+
return _handle_multi_record_err("LocalDpMod", msg, ArrayRecord)
|
|
108
|
+
|
|
109
|
+
# Record array record communicated to client and clipping norm
|
|
110
|
+
original_array_record = next(iter(msg.content.array_records.values()))
|
|
111
|
+
|
|
112
|
+
# Call inner app
|
|
113
|
+
out_msg = call_next(msg, ctxt)
|
|
114
|
+
|
|
115
|
+
# Check if the msg has error
|
|
116
|
+
if out_msg.has_error():
|
|
117
|
+
return out_msg
|
|
118
|
+
|
|
119
|
+
# Ensure reply has a single ArrayRecord
|
|
120
|
+
if len(out_msg.content.array_records) != 1:
|
|
121
|
+
return _handle_multi_record_err("LocalDpMod", out_msg, ArrayRecord)
|
|
122
|
+
|
|
123
|
+
new_array_record_key, client_to_server_arrecord = next(
|
|
124
|
+
iter(out_msg.content.array_records.items())
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Ensure keys in returned ArrayRecord match those in the one sent from server
|
|
128
|
+
if list(original_array_record.keys()) != list(client_to_server_arrecord.keys()):
|
|
129
|
+
return _handle_array_key_mismatch_err("LocalDpMod", out_msg)
|
|
130
|
+
|
|
131
|
+
client_to_server_ndarrays = client_to_server_arrecord.to_numpy_ndarrays()
|
|
132
|
+
|
|
133
|
+
# Clip the client update
|
|
134
|
+
compute_clip_model_update(
|
|
135
|
+
client_to_server_ndarrays,
|
|
136
|
+
original_array_record.to_numpy_ndarrays(),
|
|
137
|
+
self.clipping_norm,
|
|
138
|
+
)
|
|
139
|
+
log(
|
|
140
|
+
INFO,
|
|
141
|
+
"LocalDpMod: parameters are clipped by value: %.4f.",
|
|
142
|
+
self.clipping_norm,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
std_dev = (
|
|
146
|
+
self.sensitivity * np.sqrt(2 * np.log(1.25 / self.delta)) / self.epsilon
|
|
147
|
+
)
|
|
148
|
+
add_gaussian_noise_inplace(
|
|
149
|
+
client_to_server_ndarrays,
|
|
150
|
+
std_dev,
|
|
151
|
+
)
|
|
152
|
+
log(
|
|
153
|
+
INFO,
|
|
154
|
+
"LocalDpMod: local DP noise with %.4f stddev added to parameters",
|
|
155
|
+
std_dev,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Replace outgoing ArrayRecord's Array while preserving their keys
|
|
159
|
+
out_msg.content[new_array_record_key] = ArrayRecord(
|
|
160
|
+
OrderedDict(
|
|
161
|
+
{
|
|
162
|
+
k: Array(v)
|
|
163
|
+
for k, v in zip(
|
|
164
|
+
client_to_server_arrecord.keys(), client_to_server_ndarrays
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
return out_msg
|
flwr/clientapp/typing.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
"""Custom types for Flower clients."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from typing import Callable
|
|
19
|
+
|
|
20
|
+
from flwr.common import Context, Message
|
|
21
|
+
|
|
22
|
+
ClientAppCallable = Callable[[Message, Context], Message]
|
|
@@ -19,7 +19,7 @@ from logging import DEBUG
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Callable, Optional
|
|
21
21
|
|
|
22
|
-
from flwr.
|
|
22
|
+
from flwr.clientapp.client_app import ClientApp, LoadClientAppError
|
|
23
23
|
from flwr.common.config import (
|
|
24
24
|
get_flwr_dir,
|
|
25
25
|
get_metadata_from_config,
|
flwr/common/constant.py
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import os
|
|
21
|
+
|
|
20
22
|
TRANSPORT_TYPE_GRPC_BIDI = "grpc-bidi"
|
|
21
23
|
TRANSPORT_TYPE_GRPC_RERE = "grpc-rere"
|
|
22
24
|
TRANSPORT_TYPE_GRPC_ADAPTER = "grpc-adapter"
|
|
@@ -60,7 +62,9 @@ HEARTBEAT_DEFAULT_INTERVAL = 30
|
|
|
60
62
|
HEARTBEAT_CALL_TIMEOUT = 5
|
|
61
63
|
HEARTBEAT_BASE_MULTIPLIER = 0.8
|
|
62
64
|
HEARTBEAT_RANDOM_RANGE = (-0.1, 0.1)
|
|
63
|
-
|
|
65
|
+
HEARTBEAT_MIN_INTERVAL = 10
|
|
66
|
+
HEARTBEAT_MAX_INTERVAL = 1800 # 30 minutes
|
|
67
|
+
HEARTBEAT_INTERVAL_INF = 1e300 # Large value, disabling heartbeats
|
|
64
68
|
HEARTBEAT_PATIENCE = 2
|
|
65
69
|
RUN_FAILURE_DETAILS_NO_HEARTBEAT = "No heartbeat received from the run."
|
|
66
70
|
|
|
@@ -70,13 +74,23 @@ NODE_ID_NUM_BYTES = 8
|
|
|
70
74
|
|
|
71
75
|
# Constants for FAB
|
|
72
76
|
APP_DIR = "apps"
|
|
73
|
-
FAB_ALLOWED_EXTENSIONS = {".py", ".toml", ".md"}
|
|
74
77
|
FAB_CONFIG_FILE = "pyproject.toml"
|
|
75
78
|
FAB_DATE = (2024, 10, 1, 0, 0, 0)
|
|
76
79
|
FAB_HASH_TRUNCATION = 8
|
|
77
80
|
FAB_MAX_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
78
81
|
FLWR_DIR = ".flwr" # The default Flower directory: ~/.flwr/
|
|
79
82
|
FLWR_HOME = "FLWR_HOME" # If set, override the default Flower directory
|
|
83
|
+
# FAB file include patterns (gitignore-style patterns)
|
|
84
|
+
FAB_INCLUDE_PATTERNS = (
|
|
85
|
+
"**/*.py",
|
|
86
|
+
"**/*.toml",
|
|
87
|
+
"**/*.md",
|
|
88
|
+
)
|
|
89
|
+
# FAB file exclude patterns (gitignore-style patterns)
|
|
90
|
+
FAB_EXCLUDE_PATTERNS = (
|
|
91
|
+
"**/__pycache__/**",
|
|
92
|
+
FAB_CONFIG_FILE, # Exclude the original pyproject.toml
|
|
93
|
+
)
|
|
80
94
|
|
|
81
95
|
# Constant for SuperLink
|
|
82
96
|
SUPERLINK_NODE_ID = 1
|
|
@@ -109,14 +123,14 @@ LOG_UPLOAD_INTERVAL = 0.2 # Minimum interval between two log uploads
|
|
|
109
123
|
# Retry configurations
|
|
110
124
|
MAX_RETRY_DELAY = 20 # Maximum delay duration between two consecutive retries.
|
|
111
125
|
|
|
112
|
-
# Constants for
|
|
126
|
+
# Constants for account authentication
|
|
113
127
|
CREDENTIALS_DIR = ".credentials"
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
AUTHN_TYPE_JSON_KEY = "authn-type" # For key name in JSON file
|
|
129
|
+
AUTHN_TYPE_YAML_KEY = "authn_type" # For key name in YAML file
|
|
116
130
|
ACCESS_TOKEN_KEY = "flwr-oidc-access-token"
|
|
117
131
|
REFRESH_TOKEN_KEY = "flwr-oidc-refresh-token"
|
|
118
132
|
|
|
119
|
-
# Constants for
|
|
133
|
+
# Constants for account authorization
|
|
120
134
|
AUTHZ_TYPE_YAML_KEY = "authz_type" # For key name in YAML file
|
|
121
135
|
|
|
122
136
|
# Constants for node authentication
|
|
@@ -135,7 +149,9 @@ GC_THRESHOLD = 200_000_000 # 200 MB
|
|
|
135
149
|
# Constants for Inflatable
|
|
136
150
|
HEAD_BODY_DIVIDER = b"\x00"
|
|
137
151
|
HEAD_VALUE_DIVIDER = " "
|
|
138
|
-
|
|
152
|
+
FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE = int(
|
|
153
|
+
os.getenv("FLWR_PRIVATE_MAX_ARRAY_CHUNK_SIZE", "5242880")
|
|
154
|
+
) # 5 MB
|
|
139
155
|
|
|
140
156
|
# Constants for serialization
|
|
141
157
|
INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
|
|
@@ -144,8 +160,12 @@ INT64_MAX_VALUE = 9223372036854775807 # (1 << 63) - 1
|
|
|
144
160
|
FLWR_APP_TOKEN_LENGTH = 128 # Length of the token used
|
|
145
161
|
|
|
146
162
|
# Constants for object pushing and pulling
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES = int(
|
|
164
|
+
os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PUSHES", "2")
|
|
165
|
+
) # Default maximum number of concurrent pushes
|
|
166
|
+
FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS = int(
|
|
167
|
+
os.getenv("FLWR_PRIVATE_MAX_CONCURRENT_OBJ_PULLS", "2")
|
|
168
|
+
) # Default maximum number of concurrent pulls
|
|
149
169
|
PULL_MAX_TIME = 7200 # Default maximum time to wait for pulling objects
|
|
150
170
|
PULL_MAX_TRIES_PER_OBJECT = 500 # Default maximum number of tries to pull an object
|
|
151
171
|
PULL_INITIAL_BACKOFF = 1 # Initial backoff time for pulling objects
|
|
@@ -154,7 +174,13 @@ PULL_BACKOFF_CAP = 10 # Maximum backoff time for pulling objects
|
|
|
154
174
|
|
|
155
175
|
# ControlServicer constants
|
|
156
176
|
RUN_ID_NOT_FOUND_MESSAGE = "Run ID not found"
|
|
157
|
-
|
|
177
|
+
NO_ACCOUNT_AUTH_MESSAGE = "ControlServicer initialized without account authentication"
|
|
178
|
+
NO_ARTIFACT_PROVIDER_MESSAGE = "ControlServicer initialized without artifact provider"
|
|
179
|
+
PULL_UNFINISHED_RUN_MESSAGE = "Cannot pull artifacts for an unfinished run"
|
|
180
|
+
SUPERNODE_NOT_CREATED_FROM_CLI_MESSAGE = "Invalid SuperNode credentials"
|
|
181
|
+
PUBLIC_KEY_ALREADY_IN_USE_MESSAGE = "Public key already in use"
|
|
182
|
+
PUBLIC_KEY_NOT_VALID = "The provided public key is not valid"
|
|
183
|
+
NODE_NOT_FOUND_MESSAGE = "Node ID not found for account"
|
|
158
184
|
|
|
159
185
|
|
|
160
186
|
class MessageType:
|
|
@@ -200,6 +226,7 @@ class ErrorCode:
|
|
|
200
226
|
MESSAGE_UNAVAILABLE = 3
|
|
201
227
|
REPLY_MESSAGE_UNAVAILABLE = 4
|
|
202
228
|
NODE_UNAVAILABLE = 5
|
|
229
|
+
MOD_FAILED_PRECONDITION = 6
|
|
203
230
|
|
|
204
231
|
def __new__(cls) -> ErrorCode:
|
|
205
232
|
"""Prevent instantiation."""
|
|
@@ -242,12 +269,23 @@ class CliOutputFormat:
|
|
|
242
269
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
243
270
|
|
|
244
271
|
|
|
245
|
-
class
|
|
246
|
-
"""
|
|
272
|
+
class AuthnType:
|
|
273
|
+
"""Account authentication types."""
|
|
247
274
|
|
|
275
|
+
NOOP = "noop"
|
|
248
276
|
OIDC = "oidc"
|
|
249
277
|
|
|
250
|
-
def __new__(cls) ->
|
|
278
|
+
def __new__(cls) -> AuthnType:
|
|
279
|
+
"""Prevent instantiation."""
|
|
280
|
+
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class AuthzType:
|
|
284
|
+
"""Account authorization types."""
|
|
285
|
+
|
|
286
|
+
NOOP = "noop"
|
|
287
|
+
|
|
288
|
+
def __new__(cls) -> AuthzType:
|
|
251
289
|
"""Prevent instantiation."""
|
|
252
290
|
raise TypeError(f"{cls.__name__} cannot be instantiated.")
|
|
253
291
|
|
|
@@ -278,3 +316,8 @@ class ExecPluginType:
|
|
|
278
316
|
"""Return all SuperExec plugin types."""
|
|
279
317
|
# Filter all constants (uppercase) of the class
|
|
280
318
|
return [v for k, v in vars(ExecPluginType).items() if k.isupper()]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# Constants for No-op auth plugins
|
|
322
|
+
NOOP_FLWR_AID = "<none>"
|
|
323
|
+
NOOP_ACCOUNT_NAME = "sys_noauth"
|
flwr/common/exit/exit_code.py
CHANGED
|
@@ -41,10 +41,15 @@ class ExitCode:
|
|
|
41
41
|
|
|
42
42
|
# SuperNode-specific exit codes (300-399)
|
|
43
43
|
SUPERNODE_REST_ADDRESS_INVALID = 300
|
|
44
|
-
SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301
|
|
45
|
-
|
|
44
|
+
# SUPERNODE_NODE_AUTH_KEYS_REQUIRED = 301 --- DELETED ---
|
|
45
|
+
SUPERNODE_NODE_AUTH_KEY_INVALID = 302
|
|
46
|
+
SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED = 303
|
|
46
47
|
|
|
47
48
|
# SuperExec-specific exit codes (400-499)
|
|
49
|
+
SUPEREXEC_INVALID_PLUGIN_CONFIG = 400
|
|
50
|
+
|
|
51
|
+
# FlowerCLI-specific exit codes (500-599)
|
|
52
|
+
FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID = 500
|
|
48
53
|
|
|
49
54
|
# Common exit codes (600-699)
|
|
50
55
|
COMMON_ADDRESS_INVALID = 600
|
|
@@ -101,17 +106,26 @@ EXIT_CODE_HELP = {
|
|
|
101
106
|
"When using the REST API, please provide `https://` or "
|
|
102
107
|
"`http://` before the server address (e.g. `http://127.0.0.1:8080`)"
|
|
103
108
|
),
|
|
104
|
-
ExitCode.
|
|
105
|
-
"Node authentication requires
|
|
106
|
-
"
|
|
107
|
-
"to be provided (providing only one of them is not sufficient)."
|
|
108
|
-
),
|
|
109
|
-
ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID: (
|
|
110
|
-
"Node authentication requires elliptic curve private and public key pair. "
|
|
111
|
-
"Please ensure that the file path points to a valid private/public key "
|
|
109
|
+
ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID: (
|
|
110
|
+
"Node authentication requires elliptic curve private key. "
|
|
111
|
+
"Please ensure that the file path points to a valid private key "
|
|
112
112
|
"file and try again."
|
|
113
113
|
),
|
|
114
|
+
ExitCode.SUPERNODE_STARTED_WITHOUT_TLS_BUT_NODE_AUTH_ENABLED: (
|
|
115
|
+
"The private key for SuperNode authentication was provided, but TLS is not "
|
|
116
|
+
"enabled. Node authentication can only be used when TLS is enabled."
|
|
117
|
+
),
|
|
114
118
|
# SuperExec-specific exit codes (400-499)
|
|
119
|
+
ExitCode.SUPEREXEC_INVALID_PLUGIN_CONFIG: (
|
|
120
|
+
"The YAML configuration for the SuperExec plugin is invalid."
|
|
121
|
+
),
|
|
122
|
+
# FlowerCLI-specific exit codes (500-599)
|
|
123
|
+
ExitCode.FLWRCLI_NODE_AUTH_PUBLIC_KEY_INVALID: (
|
|
124
|
+
"Node authentication requires a valid elliptic curve public key in the "
|
|
125
|
+
"SSH format and following a NIST standard elliptic curve (e.g. SECP384R1). "
|
|
126
|
+
"Please ensure that the file path points to a valid public key "
|
|
127
|
+
"file and try again."
|
|
128
|
+
),
|
|
115
129
|
# Common exit codes (600-699)
|
|
116
130
|
ExitCode.COMMON_ADDRESS_INVALID: (
|
|
117
131
|
"Please provide a valid URL, IPv4 or IPv6 address."
|