Let outside services and computers' browser notifications give bots work
A bot hook (bot_hooks) is a secret URL, POST /api/hooks/<token>, whose calls queue the hook's brief plus the raw body as a task, per user and bot like a routine. Only the token's hash is kept (shown once, rotatable); an optional HMAC-SHA256 signing secret is stored encrypted. Calls are rate limited per address and per hook, recorded in bot_hook_events (pruned daily), deduplicated by delivery id, and a call while the last run still waits joins its brief instead of queueing another. The payload is fenced data and HookRunNote tells the bot never to take instructions from it; failed runs retry like routines. A hook can instead watch a computer's browser notifications: the app installs a small watcher that owns org.freedesktop.Notifications on the sandbox's session bus, so Chrome hands it every notification (it only looks for that service at start-up, so Chrome is restarted once, never mid-task), and it posts each one to its hooks, optionally filtered. Computers reach the app at COMPUTER_APP_URL or the app container's name. Users make hooks in the conversation's Hooks modal and manage them all on /admin/routines; bots never create or change hooks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+203
@@ -20,6 +20,7 @@ import { Harnesses } from "@/src/dict/harness-dict";
|
||||
import { HarnessProviderKinds } from "@/src/dict/harness-provider-dict";
|
||||
import { BotJobKinds, BotJobUpdateKinds } from "@/src/dict/bot-queue-dict";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { BotHookEventStatuses, BotHookSignatureFormats, BotHookSources } from "@/src/dict/bot-hook-dict";
|
||||
import {
|
||||
BotFilePurposes,
|
||||
BotFileScopes,
|
||||
@@ -1139,6 +1140,208 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tableName: "bot_hooks",
|
||||
tableDescription: `A bot's hooks: a secret URL an outside service (a webhook, a script) calls to queue a task for the bot, its brief plus the call's payload. Like a routine, it belongs to one user and runs as them`,
|
||||
fields: [
|
||||
{
|
||||
fieldName: "user_id",
|
||||
fieldDescription: `The owner; runs act as them, with their memory and reach`,
|
||||
dataType: "INTEGER",
|
||||
foreignKey: {
|
||||
cascadeDelete: true,
|
||||
destinationTableName: "users",
|
||||
destinationTableColumnName: "id",
|
||||
destinationTableColumnType: "INTEGER",
|
||||
foreignKeyName: "bot_hooks_user_fk",
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: "bot_id",
|
||||
fieldDescription: `The bot that runs it`,
|
||||
dataType: "INTEGER",
|
||||
foreignKey: {
|
||||
cascadeDelete: true,
|
||||
destinationTableName: "bots",
|
||||
destinationTableColumnName: "id",
|
||||
destinationTableColumnType: "INTEGER",
|
||||
foreignKeyName: "bot_hooks_bot_fk",
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: "workspace_id",
|
||||
fieldDescription: `The workspace it runs in (context, computers, models, and the conversation its results go to); null for the bot's own chat`,
|
||||
dataType: "INTEGER",
|
||||
foreignKey: {
|
||||
cascadeDelete: true,
|
||||
destinationTableName: "workspaces",
|
||||
destinationTableColumnName: "id",
|
||||
destinationTableColumnType: "INTEGER",
|
||||
foreignKeyName: "bot_hooks_workspace_fk",
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: "title",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "instructions",
|
||||
fieldDescription: `The brief each run gets (markdown); the call's payload is added below it as data`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "computer_id",
|
||||
fieldDescription: `The computer its runs use; null for the bot's current one when it runs. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "source",
|
||||
fieldDescription: `What calls it: \`url\` (an outside service) or \`browser_notifications\` (the watcher on \`watch_computer_id\`)`,
|
||||
dataType: "TEXT",
|
||||
options: BotHookSources.map((s) => s.value),
|
||||
defaultValue: "url",
|
||||
},
|
||||
{
|
||||
fieldName: "watch_computer_id",
|
||||
fieldDescription: `For browser notifications: the computer whose notifications it gets. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "watch_filter",
|
||||
fieldDescription: `For browser notifications: only the ones whose site, title or text contains this (any case); empty takes all`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "watch_token",
|
||||
fieldDescription: `For browser notifications: the token, encrypted, so the app can give the computer's watcher its URL again after the container is recreated`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "token_prefix",
|
||||
fieldDescription: `The token's first characters, to find the hook a call names. The token itself is never stored`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "token_hash",
|
||||
fieldDescription: `SHA-256 (hex) of the whole token, compared in constant time`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "signing_secret",
|
||||
fieldDescription: `Encrypted HMAC-SHA256 secret; when set, every call must be signed with it`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "signature_header",
|
||||
fieldDescription: `The request header carrying the signature, e.g. X-Hub-Signature-256`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "signature_format",
|
||||
dataType: "TEXT",
|
||||
options: BotHookSignatureFormats.map((f) => f.value),
|
||||
},
|
||||
{
|
||||
fieldName: "enabled",
|
||||
dataType: "INTEGER",
|
||||
options: [0, 1],
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
fieldName: "run_count",
|
||||
dataType: "INTEGER",
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
fieldName: "last_called_at",
|
||||
fieldDescription: `Unix ms of the last call taken`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "last_job_id",
|
||||
fieldDescription: `The \`bots_queue\` job of the last run. Not a foreign key: finished jobs are pruned`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "last_error",
|
||||
fieldDescription: `Why the last call couldn't run or the last run failed; cleared by the next run`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
indexName: "idx_bot_hooks_token_prefix",
|
||||
indexTableFields: ["token_prefix"],
|
||||
},
|
||||
{
|
||||
indexName: "idx_bot_hooks_bot_id",
|
||||
indexTableFields: ["bot_id"],
|
||||
},
|
||||
{
|
||||
indexName: "idx_bot_hooks_user_id",
|
||||
indexTableFields: ["user_id"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tableName: "bot_hook_events",
|
||||
tableDescription: `Every call to a bot's hook and what became of it, for the hook's recent calls. Pruned after \`AppData.BotHookEventsKeepDays\``,
|
||||
fields: [
|
||||
{
|
||||
fieldName: "hook_id",
|
||||
dataType: "INTEGER",
|
||||
foreignKey: {
|
||||
cascadeDelete: true,
|
||||
destinationTableName: "bot_hooks",
|
||||
destinationTableColumnName: "id",
|
||||
destinationTableColumnType: "INTEGER",
|
||||
foreignKeyName: "bot_hook_events_hook_fk",
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: "job_id",
|
||||
fieldDescription: `The \`bots_queue\` job it went to. Not a foreign key: finished jobs are pruned`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "payload",
|
||||
fieldDescription: `The body as received, cut to \`AppData.BotHookMaxPayloadBytes\``,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "content_type",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "source_ip",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "idempotency_key",
|
||||
fieldDescription: `The sender's delivery id, so a retried call runs once`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "status",
|
||||
dataType: "TEXT",
|
||||
options: BotHookEventStatuses.map((s) => s.value),
|
||||
},
|
||||
{
|
||||
fieldName: "error",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
indexName: "idx_bot_hook_events_hook_id_created_at",
|
||||
indexTableFields: ["hook_id", "created_at"],
|
||||
},
|
||||
{
|
||||
indexName: "idx_bot_hook_events_hook_id_idempotency_key",
|
||||
indexTableFields: ["hook_id", "idempotency_key"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tableName: "workspaces",
|
||||
fields: [
|
||||
|
||||
+119
-2
@@ -17,6 +17,8 @@ export const BunSQLiteTables = [
|
||||
"bots_queue",
|
||||
"bots_queue_updates",
|
||||
"cron_jobs",
|
||||
"bot_hooks",
|
||||
"bot_hook_events",
|
||||
"workspaces",
|
||||
"workspace_bots",
|
||||
"bot_types",
|
||||
@@ -713,6 +715,121 @@ export type BUN_SQLITE_COBBLE_CRON_JOBS = {
|
||||
last_error?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_BOT_HOOKS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
/**
|
||||
* The owner; runs act as them, with their memory and reach
|
||||
*/
|
||||
user_id?: number | "";
|
||||
/**
|
||||
* The bot that runs it
|
||||
*/
|
||||
bot_id?: number | "";
|
||||
/**
|
||||
* The workspace it runs in (context, computers, models, and the conversation its results go to); null for the bot's own chat
|
||||
*/
|
||||
workspace_id?: number | "";
|
||||
title?: string;
|
||||
/**
|
||||
* The brief each run gets (markdown); the call's payload is added below it as data
|
||||
*/
|
||||
instructions?: string;
|
||||
/**
|
||||
* The computer its runs use; null for the bot's current one when it runs. Not a foreign key
|
||||
*/
|
||||
computer_id?: number | "";
|
||||
/**
|
||||
* What calls it: `url` (an outside service) or `browser_notifications` (the watcher on `watch_computer_id`)
|
||||
*/
|
||||
source?: "url" | "browser_notifications" | "";
|
||||
/**
|
||||
* For browser notifications: the computer whose notifications it gets. Not a foreign key
|
||||
*/
|
||||
watch_computer_id?: number | "";
|
||||
/**
|
||||
* For browser notifications: only the ones whose site, title or text contains this (any case); empty takes all
|
||||
*/
|
||||
watch_filter?: string;
|
||||
/**
|
||||
* For browser notifications: the token, encrypted, so the app can give the computer's watcher its URL again after the container is recreated
|
||||
*/
|
||||
watch_token?: string;
|
||||
/**
|
||||
* The token's first characters, to find the hook a call names. The token itself is never stored
|
||||
*/
|
||||
token_prefix?: string;
|
||||
/**
|
||||
* SHA-256 (hex) of the whole token, compared in constant time
|
||||
*/
|
||||
token_hash?: string;
|
||||
/**
|
||||
* Encrypted HMAC-SHA256 secret; when set, every call must be signed with it
|
||||
*/
|
||||
signing_secret?: string;
|
||||
/**
|
||||
* The request header carrying the signature, e.g. X-Hub-Signature-256
|
||||
*/
|
||||
signature_header?: string;
|
||||
signature_format?: "hex" | "sha256=hex" | "base64" | "";
|
||||
enabled?: 0 | 1 | "";
|
||||
run_count?: number | "";
|
||||
/**
|
||||
* Unix ms of the last call taken
|
||||
*/
|
||||
last_called_at?: number | "";
|
||||
/**
|
||||
* The `bots_queue` job of the last run. Not a foreign key: finished jobs are pruned
|
||||
*/
|
||||
last_job_id?: number | "";
|
||||
/**
|
||||
* Why the last call couldn't run or the last run failed; cleared by the next run
|
||||
*/
|
||||
last_error?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
hook_id?: number | "";
|
||||
/**
|
||||
* The `bots_queue` job it went to. Not a foreign key: finished jobs are pruned
|
||||
*/
|
||||
job_id?: number | "";
|
||||
/**
|
||||
* The body as received, cut to `AppData.BotHookMaxPayloadBytes`
|
||||
*/
|
||||
payload?: string;
|
||||
content_type?: string;
|
||||
source_ip?: string;
|
||||
/**
|
||||
* The sender's delivery id, so a retried call runs once
|
||||
*/
|
||||
idempotency_key?: string;
|
||||
status?: "queued" | "merged" | "rejected" | "duplicate" | "ignored" | "";
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_WORKSPACES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
@@ -1589,7 +1706,7 @@ export type BUN_SQLITE_COBBLE_LOGS = {
|
||||
/**
|
||||
* Log category used for filtering
|
||||
*/
|
||||
category?: "auth" | "websocket" | "cron" | "system" | "error" | "";
|
||||
category?: "auth" | "websocket" | "cron" | "hook" | "system" | "error" | "";
|
||||
/**
|
||||
* User who triggered the action, if any
|
||||
*/
|
||||
@@ -1984,4 +2101,4 @@ export type BUN_SQLITE_COBBLE_APP_SETTINGS = {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_TASKS & BUN_SQLITE_COBBLE_TASK_MEDIA & BUN_SQLITE_COBBLE_TASK_NOTES & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_BOT_TYPES & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_SKILLS & BUN_SQLITE_COBBLE_EXTENSION_PROPOSALS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_SKILLS & BUN_SQLITE_COBBLE_WORKSPACE_SKILLS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_TOKEN_USAGES & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS
|
||||
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_TASKS & BUN_SQLITE_COBBLE_TASK_MEDIA & BUN_SQLITE_COBBLE_TASK_NOTES & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_BOT_HOOKS & BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_BOT_TYPES & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_SKILLS & BUN_SQLITE_COBBLE_EXTENSION_PROPOSALS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_SKILLS & BUN_SQLITE_COBBLE_WORKSPACE_SKILLS & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_TOKEN_USAGES & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS
|
||||
Reference in New Issue
Block a user