webhooks #
Webhooks wake an existing session. An infra session can retain its history, memory, GitHub bot, Discord tools, and other extensions while receiving alerts; a delivery never creates a new agent. Webhooks are installed but disabled by default.
Setup #
- Open
/extensions, select webhooks, and enable it globally to mount the incoming HTTP service. Pressctrl+won the enabled entry to open this session's Webhooks screen (or type/webhooks). The slash shortcut is always in the CLI menu, even before the session command catalog loads or when the extension is off; an off extension shows guidance to enable it in/extensions. - Press
ctrl+oto add a named hook. Its session field picks the session deliveries wake; it defaults to the one you opened the screen from, and typing filters the list. The screen lists every session's hooks grouped under the session they wake, and the selected hook's wakes line names it. Leave the secret blank to generate one (it is shown once;ctrl+lcopies it) or supply a secret of 16–4096 bytes.ctrl+lcopies the selected hook's URL. The displayed URL ishttp://127.0.0.1:<webhook_port>/extensions/webhooks/hooks/<hook-id>/deliverieson the intake listener (the hook list'sdelivery_url;webhooks.list()gives it asintakeUrlbeside the bare-pathurl); it targets the chosen session, even when no TUI is attached. The hook name must use 1–64 ASCII letters, digits,_, or-. - Configure the sender to POST a body of at most 65,536 bytes with
X-Albedo-Signature: sha256=<lowercase or uppercase HMAC-SHA256 hex>computed over the exact bytes sent using the hook secret. Fixed-length and chunked uploads are supported. The header defaults tox-albedo-signature; the add/edit form sets its name and the signature prefix (for example, GitHub usesx-hub-signature-256). - For retries, send a stable
X-Albedo-Event-Id(1–128 bytes, no newlines). A retry with the same hook, ID, and body returns the original delivery ID; reuse with different bytes returns 409. Without this header, each valid POST is a separate event.
The daemon binds to 127.0.0.1. Intake has its own listener, also on 127.0.0.1, that starts at port 8088 (ALBEDO_WEBHOOK_PORT overrides it; 0 takes an OS-picked port, which test daemons use). If that port is in use, intake takes the next one up, at most 32 ports and never past 65535, so a sender's url is stable only while nothing else holds the port. Any other bind failure (a privileged port, a slow start) ends daemon boot with the reason instead of walking up. daemon.json records the bound webhook_port. When a sender goes quiet, read webhook_port from $ALBEDO_HOME/daemon.json: a second daemon on the machine lands on 8089, so 8088 may reach the other one. An intake that crashes comes back on the same port; after five failed restarts in a row (about eight seconds) it stops, the daemon keeps running, and webhook_port leaves daemon.json, the screen shows intake stopped, and the hook list's delivery_url and webhooks.list()'s intakeUrl are null until the daemon restarts. The intake listener answers only the delivery POST; every other route returns 404 there. The daemon's own port (ALBEDO_PORT) can change between starts without touching webhook urls. Configure a TLS-terminating reverse proxy that forwards only the delivery POST to the intake port if a remote service must reach it. Neither listener checks the Host header, so a proxy may forward its own; the signature is what admits a delivery. Hook management and delivery reads require the daemon bearer token. The delivery POST uses the SignedBody policy: a daemon bearer token cannot bypass its signature check. Browser requests follow the daemon's configured origin policy.
Ambiguous framing and invalid Content-Length values return 400; bodies
above 65,536 bytes return 413. A request without body framing has an empty
body. Refusals before body consumption close the connection.
Delivery #
A valid POST returns 202 and a delivery ID after durable storage, not after the agent responds. Invalid signatures return 401; unknown or disabled hooks return 404; oversized bodies return 413; a full inbox returns 429. There are at most 1,000 pending deliveries per target session. Accepted payloads remain in the inbox until the session is idle: a delivery that arrives during a turn waits in the session's queue without steering it, and every delivery and event that waited starts the next turn together when the turn ends (the same rule as Discord channel events delivered at the end of a turn; see discord.md). The dispatcher retries after a full queue or an unavailable worker, including after a daemon restart. The intake receipt and the transcript input commit in the same SQLite transaction, so a retry cannot add that receipt twice. A deleted hook does not delete already accepted deliveries; deleting the session removes its hooks and inbox.
The model sees a bounded preview labelled external data, not instructions, with the delivery ID. An agent with the extension enabled can read the full UTF-8 payload using await webhooks.delivery(id) (binary bodies report that they are binary). The inbox is not a command channel: payload text has no authority over system or user instructions.
Management #
Humans can list, add, rotate, configure the signature header/prefix, enable, disable, and delete hooks on the Webhooks screen, which also shows each hook's queued deliveries and the reason the last one was deferred. The screen's agent access setting (ctrl+t, kept visible in the browse footer on narrow terminals) applies to the session the screen was opened from and is initially off. When on, that session's agent may use await webhooks.list(), create(name, secret=None), rotate(id, revision=..., secret=None), configure(id, revision=..., header=..., prefix="sha256="), enable, disable, and delete. Management checks the permission and target session in the daemon on every call. delivery(id) remains available when management is off so the session can read its own alerts. Disabling management does not turn off existing hooks. Changes use revisions to reject stale updates.
Generated secrets are returned only on create and rotate. Stored secrets are not included in listings. The screen masks entered secrets, but entering a secret through a typed command or an agent tool may leave it in that caller's history; use the screen for human-supplied secrets. This switch limits the webhooks API, not an agent with unrestricted filesystem or shell access to Albedo's home directory.