# Copyright (c) 2026, NVIDIA CORPORATION. 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.
import os
from types import MappingProxyType
from typing import Dict, Mapping, Optional, Tuple
# _ERROR_REGISTRY is a plain mutable dict so entries can be added simply at module load time.
# It is never exported; callers must use ERROR_REGISTRY (the frozen public view) or the
# helper functions below. The two-name pattern avoids accidental mutation from call sites
# while keeping the definition readable.
_ERROR_REGISTRY: Dict[str, Dict[str, str]] = {
# --- General ---
"CONNECTION_FAILED": {
"message": "Could not connect to the FLARE server.",
"hint": "Check server status with 'nvflare system status'.",
},
"AUTH_FAILED": {
"message": "Authentication failed.",
"hint": "Check startup kit credentials.",
},
"TIMEOUT": {
"message": "Operation timed out.",
"hint": "Increase --timeout or check server load.",
},
"INVALID_ARGS": {
"message": "Invalid arguments.",
"hint": "Run the command with -h or --help for usage.",
},
"STARTUP_KIT_MISSING": {
"message": "Startup kit not found.",
"hint": "Run 'nvflare config list' and 'nvflare config use <id>', pass --kit-id <id> or --startup-kit <path>, or set NVFLARE_STARTUP_KIT_DIR for automation.",
},
"SITE_NOT_FOUND": {
"message": "Site '{site}' is not connected.",
"hint": "Use 'nvflare system status' to list connected sites.",
},
"LOG_CONFIG_INVALID": {
"message": "Log config is not valid JSON or a recognised log mode.",
"hint": "Supply a valid dictConfig JSON file or one of: DEBUG, INFO, WARNING, ERROR, CRITICAL, concise, msg_only, full, verbose, reload.",
},
"SERVER_UNREACHABLE": {
"message": "Server stopped or job ended before command was delivered.",
"hint": "Check server status with 'nvflare system status'.",
},
"SYSTEM_NOT_READY": {
"message": "FLARE system is not ready yet.",
"hint": "Wait for clients to connect, then retry 'nvflare system status'. If this persists, check POC service logs or client logs.",
},
"INTERNAL_ERROR": {
"message": "An unexpected error occurred.",
"hint": "This is likely a bug. Re-run in a development environment for a traceback, or report the issue.",
},
"CLI_ERROR": {
"message": "Command failed.",
"hint": "",
},
"STUDY_NOT_FOUND": {
"message": "Study '{study}' not found.",
"hint": "Verify the study name. If the study exists and you expect access, contact a project_admin.",
},
"STUDY_ALREADY_EXISTS": {
"message": "Study '{study}' already exists.",
"hint": "Use 'nvflare study show {study}' or contact a project_admin to update access.",
},
"STUDY_HAS_JOBS": {
"message": "Study '{study}' has associated jobs and cannot be removed.",
"hint": "Archive or delete the associated jobs before retrying.",
},
"INVALID_STUDY_NAME": {
"message": "Invalid study name '{study}'.",
"hint": "Use only lowercase letters, numbers, underscores, and hyphens.",
},
"INVALID_SITE": {
"message": "Invalid site value.",
"hint": "Use a comma-separated list of valid site names.",
},
"USER_ALREADY_IN_STUDY": {
"message": "User '{user}' is already in study '{study}'.",
"hint": "Use a different user or remove the existing entry first.",
},
"USER_NOT_IN_STUDY": {
"message": "User '{user}' is not in study '{study}'.",
"hint": "Use 'nvflare study add-user' to add the user first.",
},
"STARTUP_KIT_NOT_CONFIGURED": {
"message": "No active startup kit is configured.",
"hint": "Run 'nvflare config list' and 'nvflare config use <id>', pass --kit-id <id> or --startup-kit <path>, or set NVFLARE_STARTUP_KIT_DIR for automation.",
},
"LOCK_TIMEOUT": {
"message": "Study registry is busy.",
"hint": "Another study mutation is in progress. Retry shortly.",
},
"NOT_AUTHORIZED": {
"message": "Not authorized for this operation.",
"hint": "Use a startup kit with the required admin role.",
},
# --- Job commands ---
"JOB_NOT_FOUND": {
"message": "Job '{job_id}' does not exist.",
"hint": "Use 'nvflare job list' to see available job IDs.",
},
"JOB_NOT_RUNNING": {
"message": "Job '{job_id}' is not currently running.",
"hint": "Use 'nvflare job list' to check job status.",
},
"JOB_NOT_DONE": {
"message": "Job '{job_id}' has not finished.",
"hint": "Use 'nvflare job wait <job_id>' or 'nvflare job monitor <job_id>' before downloading results.",
},
"JOB_INVALID": {
"message": "Job folder is not a valid NVFlare job.",
"hint": "Check meta.json and config_fed_server.json.",
},
"SUBMIT_TOKEN_CONFLICT": {
"message": "A job with this submit token already exists with different content.",
"hint": (
"Use a new submit token when submitting different job content, "
"or resubmit identical job content to reuse the existing job."
),
},
"SUBMIT_TOKEN_JOB_DELETED": {
"message": "This submit token refers to a deleted job.",
"hint": "Use a new submit token to submit the job again.",
},
"LOG_NOT_FOUND": {
"message": "Job logs are not available for site '{site}'.",
"hint": "Verify that client log streaming is enabled and that the site has run this job.",
},
# --- Cert commands ---
"OUTPUT_DIR_NOT_WRITABLE": {
"message": "Cannot write to output directory {path}.",
"hint": "Check directory permissions or choose a different output directory.",
},
"CERT_GENERATION_FAILED": {
"message": "Failed to generate certificate.",
"hint": "Check that the cryptography package is installed and up-to-date.",
},
"CA_ALREADY_EXISTS": {
"message": "Root CA already exists at {path}.",
"hint": "Use --force to overwrite, or choose a different output directory.",
},
"CA_NOT_FOUND": {
"message": "No root CA found at {ca_dir}.",
"hint": "Run 'nvflare cert init' first, or specify the correct --ca-dir.",
},
"CA_LOAD_FAILED": {
"message": "Failed to load root CA material from {ca_dir}.",
"hint": "Check that rootCA.pem and rootCA.key are readable, valid, and unencrypted.",
},
"CSR_NOT_FOUND": {
"message": "CSR file not found: {path}.",
"hint": "Check the path to the .csr file.",
},
"REQUEST_ZIP_NOT_FOUND": {
"message": "Request zip not found: {path}.",
"hint": "Provide the .request.zip file created by 'nvflare cert request'.",
},
"INVALID_CSR": {
"message": "Invalid or corrupt CSR file: {path}.",
"hint": "Create a new request with 'nvflare cert request'.",
},
"CERT_ALREADY_EXISTS": {
"message": "Signed certificate already exists at {path}.",
"hint": "Use --force to overwrite.",
},
"ROOTCA_ALREADY_EXISTS": {
"message": "Root CA certificate already exists at {path}.",
"hint": "Use --force to overwrite.",
},
"INVALID_CERT_TYPE": {
"message": "Invalid certificate type '{cert_type}'.",
"hint": "Use one of: client, server, org_admin, lead, member.",
},
"KEY_ALREADY_EXISTS": {
"message": "Private key already exists at {path}.",
"hint": "Use --force to overwrite, or choose a different output directory.",
},
"INVALID_NAME": {
"message": "Invalid name '{name}': {reason}",
"hint": "The name must be 64 characters or fewer and must not contain leading/trailing whitespace.",
},
"CSR_GENERATION_FAILED": {
"message": "CSR generation failed.",
"hint": "Check that the cryptography package is installed and up-to-date.",
},
"CERT_TYPE_UNKNOWN": {
"message": "Unknown certificate type in '{cert}': the certificate type is missing or unrecognized.",
"hint": "Use a signed zip produced by 'nvflare cert approve'.",
},
"CERT_SIGNING_FAILED": {
"message": "Certificate signing failed: {reason}",
"hint": "Check that the CA key and certificate are valid and not corrupted.",
},
"CERT_OUTPUT_WRITE_FAILED": {
"message": "Failed to write signed certificate output to {path}.",
"hint": "Check output directory permissions and available disk space, then retry.",
},
# --- Package commands ---
"CERT_NOT_FOUND": {
"message": "Certificate file not found: {path}.",
"hint": "Provide the signed certificate received from the Project Admin.",
},
"KEY_NOT_FOUND": {
"message": "Private key file not found: {path}.",
"hint": "Provide the private key generated by 'nvflare cert request'.",
},
"ROOTCA_NOT_FOUND": {
"message": "Root CA file not found: {path}.",
"hint": "Provide the rootCA.pem received from the Project Admin.",
},
"INVALID_ENDPOINT": {
"message": "Invalid endpoint URI: {endpoint}.",
"hint": "Use format: grpc://host:port, tcp://host:port, or http://host:port.",
},
"OUTPUT_DIR_EXISTS": {
"message": "Output directory already exists: {path}.",
"hint": "Use --force only when intentionally replacing the existing participant output.",
},
"SIGNED_ZIP_NOT_FOUND": {
"message": "Signed zip not found: {path}.",
"hint": "Provide the .signed.zip returned by 'nvflare cert approve'.",
},
"AMBIGUOUS_KEY": {
"message": "Multiple *.key files found in {path}: {files}",
"hint": "Select one participant key for this internal packaging operation.",
},
# --- Distributed provisioning: signed zip validation ---
"INVALID_SIGNED_ZIP": {
"message": "Invalid signed zip.",
"hint": "Use the .signed.zip returned by 'nvflare cert approve'.",
},
"INVALID_PROJECT_NAME": {
"message": "Invalid project name.",
"hint": "Project name must start with a letter or digit and contain only letters, digits, hyphens, underscores, or dots.",
},
"INVALID_ROOTCA_FINGERPRINT": {
"message": "Invalid root CA SHA256 fingerprint.",
"hint": "Use SHA256:AA:BB:... or OpenSSL output such as 'sha256 Fingerprint=AA:BB:...'.",
},
"ROOTCA_FINGERPRINT_MISMATCH": {
"message": "Root CA SHA256 fingerprint does not match the expected out-of-band value.",
"hint": "Verify that the signed zip came from the intended Project Admin.",
},
"SIGNED_ZIP_IDENTITY_CONFLICT": {
"message": "Signed zip identity conflict.",
"hint": "The signed zip project/org/name does not match the local request material.",
},
# --- Distributed provisioning: local site yaml ---
"LOCAL_SITE_MISMATCH": {
"message": "Local site.yaml does not match the signed zip identity.",
"hint": "Use the site.yaml created by 'nvflare cert request' for this participant.",
},
"LOCAL_SITE_INVALID": {
"message": "Local site.yaml is invalid or missing required fields.",
"hint": "Use the site.yaml created by 'nvflare cert request', or re-run 'nvflare cert request'.",
},
"LOCAL_SITE_UNSUPPORTED_FEATURE": {
"message": "Local site.yaml contains an unsupported feature.",
"hint": "Remove or update the unsupported configuration in your participant definition file.",
},
# --- Distributed provisioning: key/cert ---
"KEY_INVALID": {
"message": "Private key is invalid or corrupt.",
"hint": "Re-run 'nvflare cert request' to generate a new key pair.",
},
"KEY_CERT_MISMATCH": {
"message": "Private key does not match the signed certificate.",
"hint": "Ensure the private key from 'nvflare cert request' matches the signed zip from 'nvflare cert approve'.",
},
# --- Distributed provisioning: request directory ---
"REQUEST_DIR_NOT_FOUND": {
"message": "Request directory not found: {path}.",
"hint": "Provide the directory created by 'nvflare cert request', or omit --request-dir to auto-discover.",
},
"REQUEST_DIR_INCOMPLETE": {
"message": "Request directory is missing required local material.",
"hint": "Re-run 'nvflare cert request' to regenerate the request directory.",
},
"REQUEST_DIR_MISMATCH": {
"message": "Request directory does not match the signed zip request_id.",
"hint": "Use the directory created by 'nvflare cert request' for this signed zip.",
},
# --- Distributed provisioning: request metadata ---
"REQUEST_METADATA_NOT_FOUND": {
"message": "Request metadata (request.json) not found in the request directory.",
"hint": "Re-run 'nvflare cert request' to regenerate the request directory.",
},
"REQUEST_METADATA_INVALID": {
"message": "Request metadata (request.json) is invalid or corrupted.",
"hint": "Re-run 'nvflare cert request' to regenerate the request directory.",
},
"REQUEST_METADATA_MISMATCH": {
"message": "Request metadata does not match the signed zip.",
"hint": "Ensure the request directory matches the signed zip from 'nvflare cert approve'.",
},
# --- Distributed provisioning: project/CA binding ---
"PROJECT_CA_MISMATCH": {
"message": "Request project does not match the CA project.",
"hint": "Use a CA directory initialized for the same project as this request.",
},
"PROJECT_PROFILE_MISMATCH": {
"message": "Request project does not match the project profile.",
"hint": "Use the project_profile.yaml for the same project as this request.",
},
# --- Package build ---
"BUILD_FAILED": {
"message": "Package build failed.",
"hint": "Check builder configuration and logs for details.",
},
"UNSIGNED_JOB_REJECTED": {
"message": "Unsigned job rejected — require_signed_jobs is enabled.",
"hint": "Sign the job with an admin cert, or disable require_signed_jobs in fed_server.json.",
},
"CERT_CHAIN_INVALID": {
"message": "Certificate {cert} is not signed by root CA {rootca}.",
"hint": "Ensure the cert was signed by the Project Admin using the same root CA.",
},
"CERT_EXPIRED": {
"message": "Certificate {cert} expired at {expiry}.",
"hint": "Request a new certificate from the Project Admin.",
},
"PROJECT_FILE_NOT_FOUND": {
"message": "Project file not found: {path}.",
"hint": "Provide the path to a site-scoped project yaml file.",
},
"INVALID_PROJECT_FILE": {
"message": "Invalid project file.",
"hint": "Ensure the file is schema-compatible with 'nvflare provision' project.yaml (api_version: 3 or 4).",
},
"UNSUPPORTED_TOPOLOGY": {
"message": "Relay participants found in project file — hierarchical FL is not supported by 'nvflare package'.",
"hint": "Use 'nvflare provision' for relay topologies.",
},
"NO_PARTICIPANTS": {
"message": "No participants to build after applying type filter.",
"hint": "Check the project file and participant type filter.",
},
# --- Version ---
"VERSION_MISMATCH": {
"message": "Remote server and client sites are running different NVFlare versions.",
"hint": "Run 'nvflare system version' to see per-site versions. Run 'nvflare preflight-check' to verify compatibility.",
},
# --- Job lifecycle ---
"JOB_FAILED": {
"message": "Job '{job_id}' reached terminal state FAILED.",
"hint": "Use 'nvflare job logs <job_id>' and 'nvflare job meta <job_id>' to inspect the failure.",
},
"JOB_ABORTED": {
"message": "Job '{job_id}' was aborted.",
"hint": "Use 'nvflare job meta <job_id>' to see abort details.",
},
"JOB_FINISHED_EXCEPTION": {
"message": "Job '{job_id}' reached terminal state FINISHED_EXCEPTION.",
"hint": "Use 'nvflare job logs <job_id>' and 'nvflare job meta <job_id>' to inspect the failure.",
},
"JOB_ABANDONED": {
"message": "Job '{job_id}' was abandoned.",
"hint": "Use 'nvflare job meta <job_id>' to inspect the abandonment details.",
},
# --- Recipe / run ---
"RECIPE_ENTRY_NOT_FOUND": {
"message": "Recipe entry not found.",
"hint": "Check --entry module:symbol matches a file in --recipe-folder. Run with --entry to specify explicitly.",
},
"RECIPE_ENTRY_AMBIGUOUS": {
"message": "Multiple Recipe subclasses found; use --entry to select one.",
"hint": "Use --entry module:ClassName to select one explicitly.",
},
"RECIPE_EXPORT_FAILED": {
"message": "Recipe export failed.",
"hint": "Check the recipe.export() implementation for errors.",
},
"RECIPE_RUNNER_FAILED": {
"message": "Recipe runner failed.",
"hint": "Check the recipe class run() method for errors.",
},
}
# Freeze the registry into an immutable MappingProxyType so callers cannot accidentally
# add, remove, or overwrite entries at runtime. Use get_error_entry() / get_error() to
# look up codes; use _ERROR_REGISTRY directly only when adding new entries in this file.
ERROR_REGISTRY: Mapping[str, Mapping[str, str]] = MappingProxyType(_ERROR_REGISTRY)
[docs]
def get_error_entry(code: str) -> Optional[Mapping[str, str]]:
entry = ERROR_REGISTRY.get(code)
if entry is None and os.getenv("NVFLARE_DEV") == "1":
raise KeyError(f"Unknown CLI error code: {code}")
return entry
[docs]
def get_error(code: str, **kwargs) -> Tuple[str, str]:
"""Return (message, hint) for the given error code with placeholders filled.
Transitional helper for legacy cert/package call sites. New CLI code should prefer
output_error()/output_error_message(). Falls back to a generic tuple for unknown codes.
"""
entry = get_error_entry(code)
if entry is None:
return "Unknown error.", "Check logs for details."
template = entry["message"]
hint = entry["hint"]
if code == "CONNECTION_FAILED":
host = kwargs.get("host")
port = kwargs.get("port")
if host is not None and port is not None:
return f"Cannot connect to the FLARE server at {host}:{port}.", hint
if host is not None:
return f"Cannot connect to the FLARE server at {host}.", hint
return "Cannot connect to the FLARE server.", hint
if code == "AUTH_FAILED" and "username" in kwargs:
return f"Authentication failed for user {kwargs['username']}.", hint
if code == "TIMEOUT" and "timeout" in kwargs:
return f"Operation timed out after {kwargs['timeout']} seconds.", hint
if code == "INVALID_ARGS" and "detail" in kwargs:
return f"Invalid arguments: {kwargs['detail']}", hint
try:
message = template.format_map(kwargs)
except KeyError:
message = template
return message, hint