Webhook receiver: Plane deliveries trigger a tick (PAD-7)

agentd webhook-serve (agentd/webhook.py, stdlib): verifies X-Plane-Signature
(HMAC-SHA256 hex of the body, constant-time; 401 on missing/bad), routes by
data.project to a configured tenant (others 202), coalesces deliveries within
3 s into one tick, runs one more tick when deliveries arrive during a tick,
and retries every 15 s for up to 2 min while the lock is held (tick command
exit 75: flock -E 75 in the cron scripts, cli tick on Busy). Ticks run in a
systemd scope so restarting the receiver never kills one. Log:
<log_dir>/webhook.log; GET /healthz.

Config: webhook section (key names only, PLANE_WEBHOOK_SECRET scrubbed from
child envs like the Plane tokens), notify: {kind: none} as a no-op hook
called on every state move; the unused per-project webhook_secret_key is
gone (Plane webhooks are workspace-level). systemd unit for the LXC and
tools/plane-webhook.py to create/rotate the Plane webhook as the workspace
admin (secret straight into the Vault, never printed).
This commit is contained in:
jj.wang
2026-10-21 13:24:04 +00:00
parent 72681db46b
commit 6dc7e72ac8
14 changed files with 651 additions and 9 deletions
+15
View File
@@ -38,6 +38,14 @@ def cmd_tick(cfg, a):
d.tick(project=a.project)
except Busy as exc:
print("skip: %s" % exc)
sys.exit(75) # same code as the cron script's flock conflict: the webhook receiver retries later
def cmd_webhook_serve(cfg, a):
if not cfg.webhook:
sys.exit("no webhook section in %s" % cfg.path)
from .webhook import serve
serve(cfg, a.bind, a.port)
def cmd_new(cfg, a, pl=None, quiet=False):
@@ -159,6 +167,9 @@ def cmd_check_config(cfg, a):
print("terminal %-10s %-13s %-8s executors=%s token %s: %s" % (
name, t["transport"], t["os"], ",".join(t["executors"]), env, "set" if os.environ.get(env) else "MISSING"))
print("cron/exec secret args: %s" % " ".join(cfg.secret_env_args()))
if cfg.webhook:
k = cfg.webhook["secret_key"]
print("webhook-serve secret arg: --secret-env %s=%s (%s)" % (k, k, "set" if os.environ.get(k) else "not in this env"))
def parser():
@@ -205,6 +216,10 @@ def parser():
help="do not tick here; only wait for the scheduled dispatcher (use when it runs on another host)")
s.add_argument("--as", dest="as_")
s.set_defaults(fn=cmd_smoke)
s = sub.add_parser("webhook-serve", help="Plane webhook receiver: each delivery triggers a tick (agentd/webhook.py)")
s.add_argument("--bind", help="address (default: webhook.bind)")
s.add_argument("--port", type=int, help="port (default: webhook.port)")
s.set_defaults(fn=cmd_webhook_serve)
sub.add_parser("check-config", help="validate the config and list token env vars").set_defaults(fn=cmd_check_config)
return ap