feat: secure skill env setup on load (core #688)

When a skill declares required_environment_variables in its YAML
frontmatter, missing env vars trigger a secure TUI prompt (identical
to the sudo password widget) when the skill is loaded. Secrets flow
directly to ~/.hermes/.env, never entering LLM context.

Key changes:
- New required_environment_variables frontmatter field for skills
- Secure TUI widget (masked input, 120s timeout)
- Gateway safety: messaging platforms show local setup guidance
- Legacy prerequisites.env_vars normalized into new format
- Remote backend handling: conservative setup_needed=True
- Env var name validation, file permissions hardened to 0o600
- Redact patterns extended for secret-related JSON fields
- 12 existing skills updated with prerequisites declarations
- ~48 new tests covering skip, timeout, gateway, remote backends
- Dynamic panel widget sizing (fixes hardcoded width from original PR)

Cherry-picked from PR #723 by kshitijk4poor, rebased onto current main
with conflict resolution.

Fixes #688

Co-authored-by: kshitijk4poor <kshitijk4poor@users.noreply.github.com>
This commit is contained in:
kshitijk4poor 2026-03-13 03:14:04 -07:00 committed by teknium1
parent c097e56142
commit ccfbf42844
34 changed files with 2419 additions and 403 deletions

View file

@ -141,9 +141,13 @@ class TestRedactingFormatter:
def test_formats_and_redacts(self):
formatter = RedactingFormatter("%(message)s")
record = logging.LogRecord(
name="test", level=logging.INFO, pathname="", lineno=0,
name="test",
level=logging.INFO,
pathname="",
lineno=0,
msg="Key is sk-proj-abc123def456ghi789jkl012",
args=(), exc_info=None,
args=(),
exc_info=None,
)
result = formatter.format(record)
assert "abc123def456" not in result
@ -171,3 +175,15 @@ USER=teknium"""
assert "HOME=/home/user" in result
assert "SHELL=/bin/bash" in result
assert "USER=teknium" in result
class TestSecretCapturePayloadRedaction:
def test_secret_value_field_redacted(self):
text = '{"success": true, "secret_value": "sk-test-secret-1234567890"}'
result = redact_sensitive_text(text)
assert "sk-test-secret-1234567890" not in result
def test_raw_secret_field_redacted(self):
text = '{"raw_secret": "ghp_abc123def456ghi789jkl"}'
result = redact_sensitive_text(text)
assert "abc123def456" not in result