Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions agentkit/toolkit/cli/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ def config_command(
cr_repo_name: Optional[str] = typer.Option(
None, "--cr_repo_name", "--ve_cr_repo_name", help="CR repository name"
),
cr_auto_create_instance_type: Optional[str] = typer.Option(
None,
"--cr_auto_create_instance_type",
help="CR instance type when auto-creating: Micro/Enterprise",
),
# Runtime configuration parameters
runtime_name: Optional[str] = typer.Option(
None, "--runtime_name", "--ve_runtime_name", help="Runtime instance name"
Expand All @@ -135,6 +140,21 @@ def config_command(
"--ve_runtime_apikey_name",
help="Runtime API key secret name",
),
runtime_auth_type: Optional[str] = typer.Option(
None,
"--runtime_auth_type",
help="Runtime authentication type: key_auth/custom_jwt",
),
runtime_jwt_discovery_url: Optional[str] = typer.Option(
None,
"--runtime_jwt_discovery_url",
help="OIDC Discovery URL when runtime_auth_type is custom_jwt",
),
runtime_jwt_allowed_clients: Optional[List[str]] = typer.Option(
None,
"--runtime_jwt_allowed_clients",
help="Allowed OAuth2 client IDs when runtime_auth_type is custom_jwt (can be used multiple times)",
),
):
"""Configure AgentKit (supports interactive and non-interactive modes).

Expand Down Expand Up @@ -227,9 +247,13 @@ def config_command(
cr_instance_name=cr_instance_name,
cr_namespace_name=cr_namespace_name,
cr_repo_name=cr_repo_name,
cr_auto_create_instance_type=cr_auto_create_instance_type,
runtime_name=runtime_name,
runtime_role_name=runtime_role_name,
runtime_apikey_name=runtime_apikey_name,
runtime_auth_type=runtime_auth_type,
runtime_jwt_discovery_url=runtime_jwt_discovery_url,
runtime_jwt_allowed_clients=runtime_jwt_allowed_clients,
)

has_cli_params = ConfigParamHandler.has_cli_params(cli_params)
Expand Down
19 changes: 15 additions & 4 deletions agentkit/toolkit/cli/interactive_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ def _prompt_for_field(
if len(args) == 2 and type(None) in args:
field_type = args[0]

prompt_condition = metadata.get("prompt_condition")
if prompt_condition:
depends_on = prompt_condition.get("depends_on")
expected_values = prompt_condition.get("values", [])
if depends_on and expected_values:
depend_value = current_config.get(depends_on)
if depend_value not in expected_values:
return current_config.get(name, default)

if get_origin(field_type) is list or field_type is List:
return self._handle_list(description, default, metadata, current, total)

Expand All @@ -327,7 +336,6 @@ def _prompt_for_field(
description, metadata, current_config
)

# Conditional validation loop
validation = metadata.get("validation", {})
while True:
# Call specific input handler (basic validation)
Expand All @@ -339,8 +347,7 @@ def _prompt_for_field(
enhanced_description, default, metadata, current, total
)

# If conditional validation type, perform conditional validation
if validation.get("type") == "conditional" and value:
if validation.get("type") == "conditional":
errors = self._validate_conditional_value(
name, value, validation, current_config
)
Expand Down Expand Up @@ -428,7 +435,11 @@ def _validate_conditional_value(
if depend_value and depend_value in rules:
rule = rules[depend_value]

# choices validation
if rule.get("required") and (
not value or (isinstance(value, str) and not value.strip())
):
errors.append("This field cannot be empty")

if "choices" in rule and value not in rule["choices"]:
msg = rule.get(
"message", f"Must be one of: {', '.join(rule['choices'])}"
Expand Down
42 changes: 42 additions & 0 deletions agentkit/toolkit/config/config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ def collect_cli_params(
cr_instance_name: Optional[str],
cr_namespace_name: Optional[str],
cr_repo_name: Optional[str],
cr_auto_create_instance_type: Optional[str],
runtime_name: Optional[str],
runtime_role_name: Optional[str],
runtime_apikey_name: Optional[str],
runtime_auth_type: Optional[str],
runtime_jwt_discovery_url: Optional[str],
runtime_jwt_allowed_clients: Optional[List[str]],
) -> Dict[str, Any]:
"""Collect all CLI parameters.

Expand Down Expand Up @@ -144,12 +148,22 @@ def collect_cli_params(
strategy_params["cr_namespace_name"] = cr_namespace_name
if cr_repo_name is not None:
strategy_params["cr_repo_name"] = cr_repo_name
if cr_auto_create_instance_type is not None:
strategy_params["cr_auto_create_instance_type"] = (
cr_auto_create_instance_type
)
if runtime_name is not None:
strategy_params["runtime_name"] = runtime_name
if runtime_role_name is not None:
strategy_params["runtime_role_name"] = runtime_role_name
if runtime_apikey_name is not None:
strategy_params["runtime_apikey_name"] = runtime_apikey_name
if runtime_auth_type is not None:
strategy_params["runtime_auth_type"] = runtime_auth_type
if runtime_jwt_discovery_url is not None:
strategy_params["runtime_jwt_discovery_url"] = runtime_jwt_discovery_url
if runtime_jwt_allowed_clients is not None:
strategy_params["runtime_jwt_allowed_clients"] = runtime_jwt_allowed_clients

return {"common": common_params, "strategy": strategy_params}

Expand Down Expand Up @@ -230,6 +244,34 @@ def update_config(
else:
new_strategy_config[key] = value

strategy_obj = None
if strategy_name == "local":
from agentkit.toolkit.config import LocalStrategyConfig

strategy_obj = LocalStrategyConfig.from_dict(
new_strategy_config, skip_render=True
)
elif strategy_name == "cloud":
from agentkit.toolkit.config import CloudStrategyConfig

strategy_obj = CloudStrategyConfig.from_dict(
new_strategy_config, skip_render=True
)
elif strategy_name == "hybrid":
from agentkit.toolkit.config import HybridStrategyConfig

strategy_obj = HybridStrategyConfig.from_dict(
new_strategy_config, skip_render=True
)

if strategy_obj is not None:
strategy_errors = self.validator.validate_dataclass(strategy_obj)
if strategy_errors:
console.print("[red]Configuration validation failed:[/red]")
for error in strategy_errors:
console.print(f" [red]✗[/red] {error}")
return False

self._show_config_changes(
old_strategy_config,
new_strategy_config,
Expand Down
69 changes: 61 additions & 8 deletions agentkit/toolkit/config/config_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import re
from typing import List, Any
from dataclasses import fields
from dataclasses import fields, is_dataclass

from agentkit.toolkit.config.config import CommonConfig

Expand Down Expand Up @@ -82,7 +82,59 @@ def validate_common_config(config: CommonConfig) -> List[str]:
return errors

@staticmethod
def _validate_conditional_fields(config: CommonConfig) -> List[str]:
def validate_dataclass(config: Any) -> List[str]:
if not is_dataclass(config):
return []

errors: List[str] = []

for field in fields(config):
if field.name.startswith("_"):
continue

validation = field.metadata.get("validation", {})

if validation.get("type") == "conditional":
continue

value = getattr(config, field.name)

if validation.get("required") and (
not value or (isinstance(value, str) and not value.strip())
):
desc = field.metadata.get("description", field.name)
errors.append(f"{desc} is required")
continue
Comment on lines +102 to +107
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The validation logic for required fields checks "not value" which will incorrectly treat empty lists, 0, False, and other falsy values as missing. Consider using "value is None" or "value is None or value == ''" for strings specifically to avoid false positives for legitimate falsy values.

Copilot uses AI. Check for mistakes.

pattern = validation.get("pattern")
if pattern and value and isinstance(value, str):
if not re.match(pattern, value):
desc = field.metadata.get("description", field.name)
msg = validation.get("message", "Invalid format")
errors.append(f"{desc}: {msg}")

choices = field.metadata.get("choices")
if choices and value:
valid_values = []
if isinstance(choices, list):
if choices and isinstance(choices[0], dict):
valid_values = [c["value"] for c in choices]
else:
valid_values = choices

if valid_values and value not in valid_values:
desc = field.metadata.get("description", field.name)
errors.append(
f"{desc} must be one of: {', '.join(map(str, valid_values))}"
)

Comment on lines +125 to +130
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The choices validation logic doesn't handle List type fields correctly. If the field type is a List, the value will be a list and checking "value not in valid_values" will fail because it's comparing a list to individual values. This validation should check if each element of the list is in valid_values when the field is a List type.

Suggested change
if valid_values and value not in valid_values:
desc = field.metadata.get("description", field.name)
errors.append(
f"{desc} must be one of: {', '.join(map(str, valid_values))}"
)
if valid_values:
if isinstance(value, list):
invalid_items = [v for v in value if v not in valid_values]
if invalid_items:
desc = field.metadata.get("description", field.name)
errors.append(
f"{desc} contains invalid value(s): {', '.join(map(str, invalid_items))}. "
f"Allowed values are: {', '.join(map(str, valid_values))}"
)
else:
if value not in valid_values:
desc = field.metadata.get("description", field.name)
errors.append(
f"{desc} must be one of: {', '.join(map(str, valid_values))}"
)

Copilot uses AI. Check for mistakes.
conditional_errors = ConfigValidator._validate_conditional_fields(config)
errors.extend(conditional_errors)

return errors

@staticmethod
def _validate_conditional_fields(config: Any) -> List[str]:
"""Execute conditional validation (cross-field dependencies).

Args:
Expand All @@ -93,7 +145,7 @@ def _validate_conditional_fields(config: CommonConfig) -> List[str]:
"""
errors = []

for field in fields(CommonConfig):
for field in fields(config):
if field.name.startswith("_"):
continue

Expand All @@ -111,11 +163,6 @@ def _validate_conditional_fields(config: CommonConfig) -> List[str]:
depend_value = getattr(config, depends_on, None)
current_value = getattr(config, field.name, None)

if not current_value or (
isinstance(current_value, str) and not current_value.strip()
):
continue

if depend_value in rules:
rule = rules[depend_value]
field_errors = ConfigValidator._apply_conditional_rule(
Expand Down Expand Up @@ -143,6 +190,12 @@ def _apply_conditional_rule(
errors = []
desc = metadata.get("description", field_name)

if rule.get("required") and (
value is None or (isinstance(value, str) and not value.strip())
):
errors.append(f"{desc} is required")
return errors

if "choices" in rule:
if value not in rule["choices"]:
msg = rule.get(
Expand Down
50 changes: 50 additions & 0 deletions agentkit/toolkit/config/strategy_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ class HybridStrategyConfig(AutoSerializableMixin):
metadata={
"description": "CR instance type when auto-creating (Micro or Enterprise)",
"icon": "⚙️",
"choices": [
{"value": "Micro", "description": "Micro"},
{"value": "Enterprise", "description": "Enterprise"},
],
"hidden": True,
},
)
cr_image_full_url: str = field(
Expand Down Expand Up @@ -244,13 +249,33 @@ class HybridStrategyConfig(AutoSerializableMixin):
metadata={
"description": "OIDC Discovery URL for JWT validation (required when auth_type is custom_jwt)",
"examples": "https://userpool-xxx.userpool.auth.id.cn-beijing.volces.com/.well-known/openid-configuration",
"prompt_condition": {
"depends_on": "runtime_auth_type",
"values": [AUTH_TYPE_CUSTOM_JWT],
},
"validation": {
"type": "conditional",
"depends_on": "runtime_auth_type",
"rules": {
AUTH_TYPE_CUSTOM_JWT: {
"required": True,
"pattern": r"^https://.+",
"hint": "(must be a valid https URL)",
"message": "must be a valid https URL",
}
},
},
},
)
runtime_jwt_allowed_clients: List[str] = field(
default_factory=list,
metadata={
"description": "Allowed OAuth2 client IDs (required when auth_type is custom_jwt)",
"examples": "['fa99ec54-8a1c-49b2-9a9e-3f3ba31d9a33']",
"prompt_condition": {
"depends_on": "runtime_auth_type",
"values": [AUTH_TYPE_CUSTOM_JWT],
},
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The field runtime_jwt_allowed_clients has a prompt_condition but is missing a validation configuration. If this field is required when runtime_auth_type is custom_jwt (as indicated in the description), it should have a conditional validation rule similar to runtime_jwt_discovery_url. Consider adding a validation block to ensure this field is properly validated.

Suggested change
},
},
"validation": {
"type": "conditional",
"depends_on": "runtime_auth_type",
"rules": {
AUTH_TYPE_CUSTOM_JWT: {
"required": True,
"min_items": 1,
"message": "At least one allowed client ID must be specified when using custom_jwt authentication.",
}
},
},

Copilot uses AI. Check for mistakes.
},
)
runtime_endpoint: str = field(
Expand Down Expand Up @@ -371,6 +396,11 @@ class CloudStrategyConfig(AutoSerializableMixin):
metadata={
"description": "CR instance type when auto-creating (Micro or Enterprise)",
"icon": "⚙️",
"choices": [
{"value": "Micro", "description": "Micro"},
{"value": "Enterprise", "description": "Enterprise"},
],
"hidden": True,
},
)
cr_region: str = field(
Expand Down Expand Up @@ -468,13 +498,33 @@ class CloudStrategyConfig(AutoSerializableMixin):
metadata={
"description": "OIDC Discovery URL for JWT validation (required when auth_type is custom_jwt)",
"examples": "https://userpool-xxx.userpool.auth.id.cn-beijing.volces.com/.well-known/openid-configuration",
"prompt_condition": {
"depends_on": "runtime_auth_type",
"values": [AUTH_TYPE_CUSTOM_JWT],
},
"validation": {
"type": "conditional",
"depends_on": "runtime_auth_type",
"rules": {
AUTH_TYPE_CUSTOM_JWT: {
"required": True,
"pattern": r"^https://.+",
"hint": "(must be a valid https URL)",
"message": "must be a valid https URL",
}
},
},
},
)
runtime_jwt_allowed_clients: List[str] = field(
default_factory=list,
metadata={
"description": "Allowed OAuth2 client IDs (required when auth_type is custom_jwt)",
"examples": "['fa99ec54-8a1c-49b2-9a9e-3f3ba31d9a33']",
"prompt_condition": {
"depends_on": "runtime_auth_type",
"values": [AUTH_TYPE_CUSTOM_JWT],
},
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

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

The field runtime_jwt_allowed_clients has a prompt_condition but is missing a validation configuration. If this field is required when runtime_auth_type is custom_jwt (as indicated in the description), it should have a conditional validation rule similar to runtime_jwt_discovery_url. Consider adding a validation block to ensure this field is properly validated.

Suggested change
},
},
"validation": {
"type": "conditional",
"depends_on": "runtime_auth_type",
"rules": {
AUTH_TYPE_CUSTOM_JWT: {
"required": True,
"min_items": 1,
"message": "At least one allowed client ID must be specified when using custom_jwt authentication.",
}
},
},

Copilot uses AI. Check for mistakes.
},
)
runtime_endpoint: str = field(
Expand Down