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
569 changes: 569 additions & 0 deletions agent/outbound_webhooks.py

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions agent/shell_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,9 @@ def _parse_hooks_block(hooks_cfg: Any) -> List[ShellHookSpec]:
for event_name, entries in hooks_cfg.items():
# Reserved sub-keys that aren't event names — skip silently. These
# are config sub-sections nested under `hooks:` for related
# functionality (e.g. output-spill budgets).
if event_name in ("output_spill",):
# functionality (e.g. output-spill budgets, outbound webhooks —
# the latter parsed by agent/outbound_webhooks.py).
if event_name in ("output_spill", "outbound"):
continue
if event_name not in VALID_HOOKS:
suggestion = difflib.get_close_matches(
Expand Down
9 changes: 8 additions & 1 deletion cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,14 @@ def _prepare_deferred_agent_startup() -> None:
from agent.shell_hooks import register_from_config
from hermes_cli.config import load_config

register_from_config(load_config(), accept_hooks=_accept_hooks)
_hooks_cfg = load_config()
register_from_config(_hooks_cfg, accept_hooks=_accept_hooks)

from agent.outbound_webhooks import (
register_from_config as register_outbound_webhooks,
)

register_outbound_webhooks(_hooks_cfg)
except Exception:
logger.debug(
"shell-hook registration failed at deferred CLI startup",
Expand Down
8 changes: 7 additions & 1 deletion gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10689,7 +10689,13 @@ async def start(self) -> bool:
try:
from hermes_cli.config import load_config
from agent.shell_hooks import register_from_config
register_from_config(load_config(), accept_hooks=False)
_hooks_cfg = load_config()
register_from_config(_hooks_cfg, accept_hooks=False)

from agent.outbound_webhooks import (
register_from_config as register_outbound_webhooks,
)
register_outbound_webhooks(_hooks_cfg)
except Exception:
logger.debug(
"shell-hook registration failed at gateway startup",
Expand Down
94 changes: 56 additions & 38 deletions hermes_cli/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,53 +50,71 @@ def hooks_command(args) -> None:

def _cmd_list(_args) -> None:
from hermes_cli.config import load_config
from agent import shell_hooks
from agent import outbound_webhooks, shell_hooks

specs = shell_hooks.iter_configured_hooks(load_config())
cfg = load_config()
specs = shell_hooks.iter_configured_hooks(cfg)
outbound = outbound_webhooks.iter_configured_targets(cfg)

if not specs:
print("No shell hooks configured in ~/.hermes/config.yaml.")
if not specs and not outbound:
print("No shell hooks or outbound webhooks configured in ~/.hermes/config.yaml.")
print("See `hermes hooks --help` or")
print(" website/docs/user-guide/features/hooks.md")
print("for the config schema and worked examples.")
return

by_event: Dict[str, List] = {}
for spec in specs:
by_event.setdefault(spec.event, []).append(spec)

allowlist = shell_hooks.load_allowlist()
approved = {
(e.get("event"), e.get("command"))
for e in allowlist.get("approvals", [])
if isinstance(e, dict)
}

print(f"Configured shell hooks ({len(specs)} total):\n")

for event in sorted(by_event.keys()):
print(f" [{event}]")
for spec in by_event[event]:
is_approved = (spec.event, spec.command) in approved
status = "✓ allowed" if is_approved else "✗ not allowlisted"
matcher_part = f" matcher={spec.matcher!r}" if spec.matcher else ""
if not specs:
print("No shell hooks configured in ~/.hermes/config.yaml.")
else:
by_event: Dict[str, List] = {}
for spec in specs:
by_event.setdefault(spec.event, []).append(spec)

allowlist = shell_hooks.load_allowlist()
approved = {
(e.get("event"), e.get("command"))
for e in allowlist.get("approvals", [])
if isinstance(e, dict)
}

print(f"Configured shell hooks ({len(specs)} total):\n")

for event in sorted(by_event.keys()):
print(f" [{event}]")
for spec in by_event[event]:
is_approved = (spec.event, spec.command) in approved
status = "✓ allowed" if is_approved else "✗ not allowlisted"
matcher_part = f" matcher={spec.matcher!r}" if spec.matcher else ""
print(
f" - {spec.command}{matcher_part} "
f"(timeout={spec.timeout}s, {status})"
)

if is_approved:
entry = shell_hooks.allowlist_entry_for(spec.event, spec.command)
if entry and entry.get("approved_at"):
print(f" approved_at: {entry['approved_at']}")
mtime_now = shell_hooks.script_mtime_iso(spec.command)
mtime_at = entry.get("script_mtime_at_approval")
if mtime_now and mtime_at and mtime_now > mtime_at:
print(
f" ⚠ script modified since approval "
f"(was {mtime_at}, now {mtime_now}) — "
f"run `hermes hooks doctor` to re-validate"
)
print()

if outbound:
print(f"Configured outbound webhooks ({len(outbound)} total):\n")
for target in outbound:
signed = "signed" if target.secret else "UNSIGNED"
matcher_part = f" matcher={target.matcher!r}" if target.matcher else ""
print(f" - {target.label}")
print(f" url: {target.url}")
print(
f" - {spec.command}{matcher_part} "
f"(timeout={spec.timeout}s, {status})"
f" events: {', '.join(target.events)}{matcher_part} "
f"(timeout={target.timeout}s, {signed})"
)

if is_approved:
entry = shell_hooks.allowlist_entry_for(spec.event, spec.command)
if entry and entry.get("approved_at"):
print(f" approved_at: {entry['approved_at']}")
mtime_now = shell_hooks.script_mtime_iso(spec.command)
mtime_at = entry.get("script_mtime_at_approval")
if mtime_now and mtime_at and mtime_now > mtime_at:
print(
f" ⚠ script modified since approval "
f"(was {mtime_at}, now {mtime_now}) — "
f"run `hermes hooks doctor` to re-validate"
)
print()


Expand Down
9 changes: 8 additions & 1 deletion hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10806,7 +10806,14 @@ def _prepare_agent_startup(args) -> None:
from hermes_cli.config import load_config
from agent.shell_hooks import register_from_config

register_from_config(load_config(), accept_hooks=_accept_hooks)
_hooks_cfg = load_config()
register_from_config(_hooks_cfg, accept_hooks=_accept_hooks)

from agent.outbound_webhooks import (
register_from_config as register_outbound_webhooks,
)

register_outbound_webhooks(_hooks_cfg)
except Exception:
logger.debug(
"shell-hook registration failed at CLI startup",
Expand Down
Loading
Loading