feat(agent): support cli tool scoped env (#37324)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123
2026-06-11 07:14:39 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent c4a8d79be9
commit fb39df49c8
18 changed files with 406 additions and 91 deletions
+52 -25
View File
@@ -241,32 +241,9 @@ class ComposerConfigValidator:
@classmethod
def _validate_shell_config(cls, soul: dict[str, Any]) -> None:
"""Fail fast on shell env/secret/CLI config the sandbox would otherwise reject at run time."""
env = soul.get("env") or {}
seen_env_names: set[str] = set()
for section in ("variables", "secret_refs"):
entries = env.get(section)
if not isinstance(entries, list):
continue
for entry in entries:
if not isinstance(entry, dict):
continue
raw_name = entry.get("name")
if not isinstance(raw_name, str) or not raw_name.strip():
# Unnamed draft rows are tolerated; only named entries are bound to the shell.
continue
name = raw_name.strip()
if not _SHELL_ENV_NAME_PATTERN.fullmatch(name):
raise InvalidComposerConfigError(
f"env/secret name '{name}' must be a valid shell identifier (^[A-Za-z_][A-Za-z0-9_]*$)."
)
if section == "secret_refs" and cls._permission_denied(entry):
raise InvalidComposerConfigError(f"secret reference '{name}' is not authorized for this agent.")
if name in seen_env_names:
raise InvalidComposerConfigError(
f"duplicate env/secret name '{name}': environment variables and secret references "
"share the shell namespace."
)
seen_env_names.add(name)
env = soul.get("env") or {}
cls._validate_env_config(env, seen_env_names=seen_env_names, label="agent")
tools = soul.get("tools") or {}
cli_tools = tools.get("cli_tools")
@@ -284,6 +261,56 @@ class ComposerConfigValidator:
raise InvalidComposerConfigError(
"a dangerous CLI tool command must be explicitly acknowledged before save."
)
tool_name = cls._cli_tool_name(entry) or "<unnamed>"
cls._validate_env_config(
entry.get("env") or {},
seen_env_names=seen_env_names,
label=f"CLI tool '{tool_name}'",
)
@classmethod
def _validate_env_config(cls, env: Any, *, seen_env_names: set[str], label: str) -> None:
if not isinstance(env, dict):
return
for section in ("variables", "secret_refs"):
entries = env.get(section)
if not isinstance(entries, list):
continue
for entry in entries:
if not isinstance(entry, dict):
continue
name = cls._env_name(entry)
if name is None:
# Unnamed draft rows are tolerated; only named entries are bound to the shell.
continue
if not _SHELL_ENV_NAME_PATTERN.fullmatch(name):
raise InvalidComposerConfigError(
f"env/secret name '{name}' must be a valid shell identifier (^[A-Za-z_][A-Za-z0-9_]*$)."
)
if section == "secret_refs" and cls._permission_denied(entry):
raise InvalidComposerConfigError(f"secret reference '{name}' is not authorized for {label}.")
if name in seen_env_names:
raise InvalidComposerConfigError(
f"duplicate env/secret name '{name}': environment variables and secret references "
"share the shell namespace."
)
seen_env_names.add(name)
@staticmethod
def _env_name(entry: dict[str, Any]) -> str | None:
for key in ("name", "key", "env_name", "variable"):
value = entry.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return None
@staticmethod
def _cli_tool_name(entry: dict[str, Any]) -> str | None:
for key in _CLI_TOOL_NAME_KEYS:
value = entry.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return None
@classmethod
def _reject_plaintext_secrets(cls, value: Any, *, path: str) -> None: