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
11 changes: 11 additions & 0 deletions src/agentex/lib/sdk/config/environment_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,17 @@ def get_configs_for_env(self, env_target: str) -> dict[str, AgentEnvironmentConf
account_id: 6887f093600ecd59bbbd3095
helm_overrides:

The principal must contain exactly one of `user_id` or `service_account_id`.
Use `service_account_id` to register an agent under a service account
instead of a personal user identity:
dev:
kubernetes:
namespace: "sgp-000-hello-acp"
auth:
principal:
service_account_id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
account_id: 6887f093600ecd59bbbd3095

if the environment field is not explicitly set, we assume its the same as
the name of the environment
Args:
Expand Down
16 changes: 10 additions & 6 deletions src/agentex/lib/sdk/config/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,14 @@ def _validate_single_environment_config(env_name: str, env_config: AgentEnvironm

# Validate auth principal
principal = env_config.auth.principal
if not principal.get("user_id"):
raise ValueError("Auth principal must contain non-empty 'user_id'")
user_id = principal.get("user_id")
service_account_id = principal.get("service_account_id")
if not user_id and not service_account_id:
raise ValueError("Auth principal must contain non-empty 'user_id' or 'service_account_id'")
if user_id and service_account_id:
raise ValueError("Auth principal must contain only one of 'user_id' or 'service_account_id', not both")

# Check for environment-specific user_id patterns
user_id = principal["user_id"]
if isinstance(user_id, str):
if not any(env_name.lower() in user_id.lower() for env_name in ["dev", "prod", "staging", env_name]):
logger.warning(
Expand Down Expand Up @@ -233,11 +236,12 @@ def generate_helpful_error_message(error: Exception, context: str = "") -> str:
"1. Check file location: should be next to manifest.yaml\n"
"2. Verify file permissions"
)
elif "user_id" in base_msg.lower():
elif "user_id" in base_msg.lower() or "service_account_id" in base_msg.lower():
base_msg += (
"\n\n💡 Auth Principal Tips:\n"
"- user_id should be unique per environment\n"
"- Include environment name (e.g., 'dev_my_agent')\n"
"- Set exactly one of 'user_id' or 'service_account_id'\n"
"- The id should be unique per environment\n"
"- For user_id, include environment name (e.g., 'dev_my_agent')\n"
"- Use consistent naming convention across agents"
)
elif "namespace" in base_msg.lower():
Expand Down
76 changes: 76 additions & 0 deletions tests/lib/cli/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Tests for the auth-principal portion of the environments.yaml validator.

Covers the rule that an env config's principal must carry exactly one of
`user_id` or `service_account_id` — the same shape that downstream services
(agentex-auth, SGP) expect on the wire.
"""

import pytest

from agentex.lib.sdk.config.validation import (
EnvironmentsValidationError,
validate_environments_config,
)
from agentex.lib.sdk.config.environment_config import (
AgentAuthConfig,
AgentKubernetesConfig,
AgentEnvironmentConfig,
AgentEnvironmentsConfig,
)


def _config_with_principal(principal: dict) -> AgentEnvironmentsConfig:
return AgentEnvironmentsConfig(
schema_version="v1",
environments={
"dev": AgentEnvironmentConfig(
kubernetes=AgentKubernetesConfig(namespace="dev-ns"),
auth=AgentAuthConfig(principal=principal),
)
},
)


def test_user_only_principal_passes():
"""Existing user_id-only configs continue to validate (backwards compat)."""
config = _config_with_principal({"user_id": "73d0c8bd-4726-434c-9686-eb627d89f078", "account_id": "acct-1"})

validate_environments_config(config)


def test_service_account_only_principal_passes():
"""New service_account_id-only configs validate."""
config = _config_with_principal(
{"service_account_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "account_id": "acct-1"}
)

validate_environments_config(config)


def test_principal_with_neither_id_is_rejected():
"""A principal with no identity id fails fast with a clear error."""
config = _config_with_principal({"account_id": "acct-1"})

with pytest.raises(EnvironmentsValidationError) as exc_info:
validate_environments_config(config)

msg = str(exc_info.value)
assert "user_id" in msg
assert "service_account_id" in msg


def test_principal_with_both_ids_is_rejected():
"""Setting both ids is a config error — the principal must commit to one identity type."""
config = _config_with_principal(
{
"user_id": "73d0c8bd-4726-434c-9686-eb627d89f078",
"service_account_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"account_id": "acct-1",
}
)

with pytest.raises(EnvironmentsValidationError) as exc_info:
validate_environments_config(config)

msg = str(exc_info.value)
assert "only one of" in msg.lower() or "not both" in msg.lower()
Loading