@@ -182,7 +182,11 @@ def validate_config(config: Config) -> Config:
182182 "Version must be major or major.minor or major.minor.patch."
183183 )
184184 except TypeError :
185- raise click .UsageError (f"Invalid version format: { api_version } " ) from None
185+ raise click .UsageError (
186+ f"Invalid version format: { api_version } .\n \n "
187+ "Pin to a minor version, e.g.:\n "
188+ ' "api_version": "0.8"'
189+ ) from None
186190
187191 config = {
188192 "node_version" : node_version ,
@@ -220,45 +224,51 @@ def validate_config(config: Config) -> Config:
220224 if major < min_major :
221225 raise click .UsageError (
222226 f"Node.js version { node_version } is not supported. "
223- f"Minimum required version is { MIN_NODE_VERSION } ."
227+ f"Minimum required version is { MIN_NODE_VERSION } .\n \n "
228+ f"Set node_version to { MIN_NODE_VERSION } or higher:\n "
229+ f' "node_version": "{ MIN_NODE_VERSION } "'
224230 )
225231 except ValueError as e :
226232 raise click .UsageError (str (e )) from None
227233
228234 if pip_installer := config .get ("pip_installer" ):
229- if pip_installer == "uv_lock" :
230- raise click .UsageError (
231- "pip_installer 'uv_lock' has been replaced. Use "
232- '`source: {"kind": "uv", "root": "..", '
233- '"package": "my-agent"}`.'
234- )
235235 if pip_installer not in ["auto" , "pip" , "uv" ]:
236236 raise click .UsageError (
237237 f"Invalid pip_installer: '{ pip_installer } '. "
238- "Must be 'auto', 'pip', or 'uv'."
238+ "Consider using uv-based source management instead:\n \n "
239+ ' "source": {"kind": "uv", "root": ".."}'
239240 )
240241
241242 source = config .get ("source" )
242243 source_kind = _get_source_kind (config )
243244 if source is not None and not isinstance (source , dict ):
244- raise click .UsageError ("`source` must be an object." )
245+ raise click .UsageError (
246+ "`source` must be an object, e.g.:\n "
247+ ' "source": {"kind": "uv", "root": ".."}'
248+ )
245249 if source is not None and source_kind != "uv" :
246- raise click .UsageError ("Invalid source.kind. Supported values: 'uv'." )
250+ raise click .UsageError (
251+ "Invalid source.kind. The only supported value is 'uv':\n "
252+ ' "source": {"kind": "uv", "root": ".."}'
253+ )
247254
248255 if config .get ("python_version" ):
249256 pyversion = config ["python_version" ]
250257 if not pyversion .count ("." ) == 1 or not all (
251258 part .isdigit () for part in pyversion .split ("-" )[0 ].split ("." )
252259 ):
260+ parts = pyversion .split ("-" )[0 ].split ("." )
261+ fix = f"{ parts [0 ]} .{ parts [1 ]} " if len (parts ) >= 2 else MIN_PYTHON_VERSION
253262 raise click .UsageError (
254263 f"Invalid Python version format: { pyversion } . "
255- "Use 'major.minor' format (e.g., '3.11'). "
256- "Patch version cannot be specified."
264+ "Use 'major.minor' format — patch version cannot be specified. \n \n "
265+ f' "python_version": " { fix } "'
257266 )
258267 if _parse_version (pyversion ) < _parse_version (MIN_PYTHON_VERSION ):
259268 raise click .UsageError (
260269 f"Python version { pyversion } is not supported. "
261- f"Minimum required version is { MIN_PYTHON_VERSION } ."
270+ f"Minimum required version is { MIN_PYTHON_VERSION } .\n \n "
271+ f' "python_version": "{ MIN_PYTHON_VERSION } "'
262272 )
263273 if "bullseye" in pyversion :
264274 raise click .UsageError (
@@ -269,12 +279,16 @@ def validate_config(config: Config) -> Config:
269279 if source_kind != "uv" and not config ["dependencies" ]:
270280 raise click .UsageError (
271281 "No dependencies found in config. "
272- "Add at least one dependency to 'dependencies' list."
282+ "Consider using uv-based source management:\n \n "
283+ ' "source": {"kind": "uv", "root": ".."}'
273284 )
274285
275286 if not config .get ("graphs" ):
276287 raise click .UsageError (
277- "No graphs found in config. Add at least one graph to 'graphs' dictionary."
288+ "No graphs found in config. Add at least one graph, e.g.:\n "
289+ ' "graphs": {\n '
290+ ' "agent": "./my_agent/graph.py:graph"\n '
291+ " }"
278292 )
279293
280294 # Validate image_distro config
@@ -287,7 +301,8 @@ def validate_config(config: Config) -> Config:
287301 if image_distro not in Distros .__args__ :
288302 raise click .UsageError (
289303 f"Invalid image_distro: '{ image_distro } '. "
290- "Must be one of 'debian', 'wolfi', or 'bookworm'."
304+ f"Must be one of: { ', ' .join (repr (d ) for d in Distros .__args__ )} .\n \n "
305+ ' "image_distro": "wolfi" (recommended)'
291306 )
292307
293308 if source_kind == "uv" :
@@ -369,6 +384,51 @@ def validate_config(config: Config) -> Config:
369384 return config
370385
371386
387+ # Keys recognized by validate_config (used to detect unknown fields).
388+ _KNOWN_CONFIG_KEYS = {
389+ "python_version" ,
390+ "node_version" ,
391+ "api_version" ,
392+ "base_image" ,
393+ "image_distro" ,
394+ "pip_config_file" ,
395+ "pip_installer" ,
396+ "source" ,
397+ "dependencies" ,
398+ "dockerfile_lines" ,
399+ "graphs" ,
400+ "env" ,
401+ "store" ,
402+ "auth" ,
403+ "encryption" ,
404+ "http" ,
405+ "webhooks" ,
406+ "checkpointer" ,
407+ "ui" ,
408+ "ui_config" ,
409+ "keep_pkg_tools" ,
410+ # Internal / legacy (still recognized, may error separately)
411+ "_INTERNAL_docker_tag" ,
412+ "project_root" ,
413+ "package" ,
414+ }
415+
416+
417+ def get_unknown_keys (raw_config : dict ) -> list [str ]:
418+ """Return warnings for unrecognized top-level keys (typos, etc.)."""
419+ import difflib
420+
421+ unknown = set (raw_config ) - _KNOWN_CONFIG_KEYS
422+ warnings : list [str ] = []
423+ for key in sorted (unknown ):
424+ close = difflib .get_close_matches (key , _KNOWN_CONFIG_KEYS , n = 1 )
425+ if close :
426+ warnings .append (f"Unknown key '{ key } ' — did you mean '{ close [0 ]} '?" )
427+ else :
428+ warnings .append (f"Unknown key '{ key } ' is not a recognized config field." )
429+ return warnings
430+
431+
372432def validate_config_file (config_path : pathlib .Path ) -> Config :
373433 """Load and validate a configuration file."""
374434 with open (config_path ) as f :
0 commit comments