feat(secrets): add phase 1 secrets tool and redaction hardening

Implements the first pragmatic slice of issue #3627 / #410:
- add agent-facing  tool with list/check/request/delete/inject
  actions
- reuse existing secure CLI secret capture path via getpass-backed callback
  so secret values never enter model context
- support  as an alias for the existing
   skill frontmatter
- redact execute_code stdout/stderr before returning tool output
- expand redaction patterns for Twilio SIDs and JWTs
- register the new tool in discovery/core toolsets and add regression tests

Gateway DM+delete secret capture remains scoped as follow-up work per the
Phase 1 issue discussion.
This commit is contained in:
Shannon Sands
2026-03-29 09:44:17 +10:00
parent f007284d05
commit c1ef64a0ac
13 changed files with 544 additions and 2 deletions

View File

@@ -198,11 +198,34 @@ def _get_required_environment_variables(
) -> List[Dict[str, Any]]:
setup = _normalize_setup_metadata(frontmatter)
required_raw = frontmatter.get("required_environment_variables")
requires_secrets_raw = frontmatter.get("requires_secrets")
if isinstance(required_raw, dict):
required_raw = [required_raw]
if not isinstance(required_raw, list):
required_raw = []
if isinstance(requires_secrets_raw, dict):
requires_secrets_raw = [requires_secrets_raw]
if not isinstance(requires_secrets_raw, list):
requires_secrets_raw = []
# `requires_secrets` is a friendlier alias for skill authors. Normalize it
# into the existing required_environment_variables pipeline so prompt,
# validation, env passthrough, and gateway hints all work unchanged.
for item in requires_secrets_raw:
if isinstance(item, str):
required_raw.append({"name": item})
elif isinstance(item, dict):
required_raw.append(
{
"name": item.get("key") or item.get("name") or item.get("env_var"),
"prompt": item.get("prompt") or item.get("description") or item.get("label"),
"help": item.get("instructions") or item.get("help") or item.get("provider_url") or item.get("url"),
"required_for": item.get("required_for") or item.get("description"),
}
)
required: List[Dict[str, Any]] = []
seen: set[str] = set()