Files
hermes-agent/hermes_cli/subcommands/webhook.py
Teknium 45ab5e3b5d Inspired by ChatGPT Work: event-triggered cron jobs via webhook routes
ChatGPT Work's Aug 25 2026 release lets scheduled tasks fire from app
events (new Gmail message, Slack activity, GitHub PR feedback) instead
of polling on a cadence. This ports the pattern by composing two
existing Hermes subsystems: a webhook route can now set cron_job to
fire an existing cron job on each inbound event.

- gateway/platforms/webhook.py: cron_job route mode — after the same
  HMAC auth / rate limit / filters / script / idempotency as agent
  routes, the rendered prompt becomes transient per-run context and the
  job fires through execute_job_for_event on a worker thread (202
  Accepted immediately). Startup validation rejects cron_job +
  deliver_only.
- tools/cronjob_tools.py: execute_job_for_event() — public wrapper over
  the shared claimed-run body (_execute_job_now), so event fires share
  at-most-once claiming, in-flight dedupe, delivery, and [SILENT]
  handling with scheduler and manual runs.
- hermes webhook subscribe --cron-job: creates event-trigger
  subscriptions; job ref validated (and canonicalized to the job ID) at
  create time.
- Docs: webhooks.md route table + Event-Triggered Cron Jobs section,
  cron.md capability list, zh-Hans mirrors.
- Tests: tests/gateway/test_webhook_cron_trigger.py (adapter + unit),
  CLI tests in test_webhook_cli.py.
2026-09-13 21:30:02 -07:00

63 lines
3.3 KiB
Python

"""``hermes webhook`` subcommand parser."""
from __future__ import annotations
from typing import Callable
def build_webhook_parser(subparsers, *, cmd_webhook: Callable) -> None:
"""Attach the ``webhook`` subcommand to ``subparsers``."""
webhook_parser = subparsers.add_parser(
"webhook", help="Manage dynamic webhook subscriptions",
description="Create, list, and remove webhook subscriptions for event-driven agent activation",
)
webhook_subparsers = webhook_parser.add_subparsers(dest="webhook_action")
wh_sub = webhook_subparsers.add_parser(
"subscribe", aliases=["add"], help="Create a webhook subscription")
wh_sub.add_argument("name", help="Route name (used in URL: /webhooks/<name>)")
wh_sub.add_argument(
"--prompt", default="", help="Prompt template with {dot.notation} payload refs")
wh_sub.add_argument("--events", default="", help="Comma-separated event types to accept")
wh_sub.add_argument("--description", default="", help="What this subscription does")
wh_sub.add_argument("--skills", default="", help="Comma-separated skill names to load")
wh_sub.add_argument(
"--deliver", default="log", help="Delivery target: log, telegram, discord, slack, etc.")
wh_sub.add_argument(
"--deliver-chat-id", default="", help="Target chat ID for cross-platform delivery")
wh_sub.add_argument("--secret", default="", help="HMAC secret (auto-generated if omitted)")
wh_sub.add_argument(
"--route-profile", dest="route_profile", default=None, metavar="PROFILE",
help="Bind the route to a multiplexed profile: only POSTs to /p/PROFILE/webhooks/<name> "
"are accepted and the agent runs as that profile (default: default; kept on update). "
"Distinct from the global -p/--profile, which picks the gateway whose subscriptions "
"file is written.")
wh_sub.add_argument(
"--deliver-only", action="store_true",
help="Skip the agent — deliver the rendered prompt directly as the "
"message. Zero LLM cost. Requires --deliver to be a real target "
"(not 'log').")
wh_sub.add_argument(
"--script", default="",
help="Filter/transform script under ~/.hermes/scripts/. The route "
"payload is passed as JSON on stdin; empty stdout, [SILENT], or a "
"nonzero exit code ignores the webhook.")
wh_sub.add_argument(
"--cron-job", default="",
help="Fire an existing cron job (by ID or name) when this route receives an event, instead of "
"starting a fresh agent run. The rendered --prompt template is passed to the job as transient "
"per-run context; the job's own prompt, skills, and delivery settings apply. Mutually exclusive "
"with --deliver-only.")
webhook_subparsers.add_parser("list", aliases=["ls"], help="List all dynamic subscriptions")
wh_rm = webhook_subparsers.add_parser("remove", aliases=["rm"], help="Remove a subscription")
wh_rm.add_argument("name", help="Subscription name to remove")
wh_test = webhook_subparsers.add_parser("test", help="Send a test POST to a webhook route")
wh_test.add_argument("name", help="Subscription name to test")
wh_test.add_argument(
"--payload", default="", help="JSON payload to send (default: test payload)")
webhook_parser.set_defaults(func=cmd_webhook)