Keep every hook call's response, searchable by the user and the bot
A hook call used to record only how it was taken; its run's answer went to the chat alone and was lost to the call once the job was pruned. Now `bot_hook_events` keeps `response`, `response_message_id` and `responded_at`, written on every call a run took in when that run ends (completed, failed, lost, stopped or cancelled) by `recordHookCallResponse` in the job helpers, and calls turn done or failed. The Calls view searches what calls sent and what runs answered, filters by status and pages; Show all loads one call whole. Bots get `search_hook_calls` and `read_hook_call` for their own hooks (never in a hook call's own run), with payloads always marked as outside data. Calls are kept for the app setting `hook_events_keep_days` (Settings > General, default now 30 days). The caller still only gets 202 Queued. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -141,7 +141,7 @@ Server push only. `pushBotNotice` (`server:bot-message`, `server:bot-updated`),
|
||||
|
||||
### Hooks (`bot_hooks`)
|
||||
|
||||
A secret URL, `POST /api/hooks/<token>` (`src/pages/api/hooks/[token].ts`, public, no session), that queues a hook's brief plus the raw body as a task, per user and bot like a routine (runs act as the owner, who must still reach the bot and not be read-only). Only the token's prefix and SHA-256 are stored (shown once; "New URL" rotates it); an optional HMAC-SHA256 signing secret is stored encrypted (`verifyHookSignature`). Order in the route: rate limit per IP, lookup (same 404 for every miss), rate limit per hook, signature, then `fireBotHook`. Calls are recorded in `bot_hook_events` (pruned daily) with idempotency on the sender's delivery header. A call while the last run is still pending and untried joins its brief (`mergeHookCallSql`) instead of queueing another. The payload is fenced data (`hookPayloadBlock`), and `HookRunNote` tells the bot never to take instructions from it. Failed runs retry like routines and keep why in `last_error`. Users make hooks in the conversation's Hooks modal and manage all of theirs on `/admin/routines`. A hook runs in one session (`bot_hooks.session_id`, open or closed; null follows the active session): the form picks the place (the bot's own chat or one of its workspaces) and a session there, or a new one of its own, made closed beside the active one (`startHookSession`) so the chat on screen stays; a bot's proposed hook gets the session it was proposed in; a missing session is replaced on the next call (`resolveHookSession`). Changing place or session is refused while a run is open; the computer is kept only if reachable there. **A bot never makes or changes a hook on its own:** `list_hooks`, `pause_hook` and `delete_hook` act directly (they only take away); a new hook, or a new name, brief or filter, or switching one back on, goes through `propose_hook` -> `hook_proposals` -> `[hook-proposal:ID]` card, described from the data and confirmed by that user (`checkHookProposal` runs again on confirm). What calls a hook, its computers and its signing are never proposed. A new hook's URL shows on the card only, never in the outcome the bot is told. Only chat replies propose; a hook call's own run gets no hook tools.
|
||||
A secret URL, `POST /api/hooks/<token>` (`src/pages/api/hooks/[token].ts`, public, no session), that queues a hook's brief plus the raw body as a task, per user and bot like a routine (runs act as the owner, who must still reach the bot and not be read-only). Only the token's prefix and SHA-256 are stored (shown once; "New URL" rotates it); an optional HMAC-SHA256 signing secret is stored encrypted (`verifyHookSignature`). Order in the route: rate limit per IP, lookup (same 404 for every miss), rate limit per hook, signature, then `fireBotHook`. Calls are recorded in `bot_hook_events` with idempotency on the sender's delivery header; when a run ends (completed, failed, lost, stopped, cancelled) every call it took in gets its `response` and turns `done`/`failed` (`recordHookCallResponse`, called from the job helpers, not the handler). Calls are searchable by payload/response (`searchHookCallsSql`; Calls view, bot tools `search_hook_calls`/`read_hook_call`) and pruned daily after the app setting `hook_events_keep_days` (default `AppData.BotHookEventsKeepDays`). The caller only ever gets `202 Queued`. A call while the last run is still pending and untried joins its brief (`mergeHookCallSql`) instead of queueing another. The payload is fenced data (`hookPayloadBlock`), and `HookRunNote` tells the bot never to take instructions from it. Failed runs retry like routines and keep why in `last_error`. Users make hooks in the conversation's Hooks modal and manage all of theirs on `/admin/routines`. A hook runs in one session (`bot_hooks.session_id`, open or closed; null follows the active session): the form picks the place (the bot's own chat or one of its workspaces) and a session there, or a new one of its own, made closed beside the active one (`startHookSession`) so the chat on screen stays; a bot's proposed hook gets the session it was proposed in; a missing session is replaced on the next call (`resolveHookSession`). Changing place or session is refused while a run is open; the computer is kept only if reachable there. **A bot never makes or changes a hook on its own:** `list_hooks`, `search_hook_calls`, `read_hook_call`, `pause_hook` and `delete_hook` act directly (they only read or take away); a new hook, or a new name, brief or filter, or switching one back on, goes through `propose_hook` -> `hook_proposals` -> `[hook-proposal:ID]` card, described from the data and confirmed by that user (`checkHookProposal` runs again on confirm). What calls a hook, its computers and its signing are never proposed. A new hook's URL shows on the card only, never in the outcome the bot is told. Only chat replies propose; a hook call's own run gets no hook tools.
|
||||
|
||||
**Browser notifications:** a hook with `source = browser_notifications` is called by a watcher on `watch_computer_id` instead of an outside service. The watcher (`src/data/notify-watch-script.ts`, Python + GLib, run as `gem` from `~/.cobble/`) owns `org.freedesktop.Notifications` on the sandbox's session bus, so Chrome hands it every notification, push ones included, instead of its silent built-in message center, and it posts each as JSON (`origin`, `title`, `body`) to the URLs in `~/.cobble/notify-hooks`. Chrome only looks for that service when it starts, so `syncComputerNotifyWatch` restarts Chrome once after starting the watcher (its sandbox supervisor brings it back), never while a task works on that computer. The hook's token is kept encrypted (`watch_token`) to rewrite that list after a container is recreated; `sync-notify-watchers` (every 5 minutes) and every change to such a hook resync it. `watch_filter` keeps only matching notifications (others are recorded as `ignored`). Computers reach the app at `COMPUTER_APP_URL` (an app setting; needed for a dev server outside docker), else the app container's name on the docker network. Sites still need notification permission in that computer's browser (a `Browser.grantPermissions` over CDP lasts only for that CDP session). Anything on the computer can read the list and call those hooks, which is one more reason payloads are only data.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user