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:
@@ -0,0 +1,55 @@
|
||||
import { z } from "zod";
|
||||
import { BotHookSignatureFormats, BotHookSources } from "@/src/dict/bot-hook-dict";
|
||||
|
||||
const HookIdField = z.coerce.number().int().positive("Pick a hook");
|
||||
const TitleField = z.string().trim().min(1, "Give the hook a name").max(80, "Keep the name under 80 characters");
|
||||
const InstructionsField = z.string().trim().min(1, "Tell the bot what to do with each call").max(8000);
|
||||
const SignatureFormatField = z.enum(BotHookSignatureFormats.map((f) => f.value) as [string, ...string[]]);
|
||||
/**
|
||||
* An HTTP header name (RFC 7230 token characters).
|
||||
*/
|
||||
const SignatureHeaderField = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100)
|
||||
.regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]*$/, "That isn't a header name");
|
||||
const SourceField = z.enum(BotHookSources.map((s) => s.value) as [string, ...string[]]);
|
||||
const WatchFilterField = z.string().trim().max(300, "Keep the filter under 300 characters");
|
||||
const SigningSecretField = z.string().trim().min(8, "Use a secret of at least 8 characters").max(500);
|
||||
|
||||
export const CreateBotHookRequestSchema = z.object({
|
||||
bot_id: z.coerce.number().int().positive("Pick a bot"),
|
||||
workspace_id: z.coerce.number().int().positive().nullable().optional(),
|
||||
title: TitleField,
|
||||
instructions: InstructionsField,
|
||||
signing_secret: SigningSecretField.nullable().optional(),
|
||||
signature_header: SignatureHeaderField.optional(),
|
||||
signature_format: SignatureFormatField.optional(),
|
||||
source: SourceField.optional(),
|
||||
watch_computer_id: z.coerce.number().int().positive().nullable().optional(),
|
||||
watch_filter: WatchFilterField.nullable().optional(),
|
||||
});
|
||||
|
||||
export type CreateBotHookRequest = z.infer<typeof CreateBotHookRequestSchema>;
|
||||
|
||||
export const UpdateBotHookRequestSchema = z.object({
|
||||
hook_id: HookIdField,
|
||||
title: TitleField.optional(),
|
||||
instructions: InstructionsField.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
/**
|
||||
* `null` stops checking signatures.
|
||||
*/
|
||||
signing_secret: SigningSecretField.nullable().optional(),
|
||||
signature_header: SignatureHeaderField.optional(),
|
||||
signature_format: SignatureFormatField.optional(),
|
||||
watch_filter: WatchFilterField.optional(),
|
||||
});
|
||||
|
||||
export type UpdateBotHookRequest = z.infer<typeof UpdateBotHookRequestSchema>;
|
||||
|
||||
export const BotHookIdRequestSchema = z.object({
|
||||
hook_id: HookIdField,
|
||||
});
|
||||
|
||||
export type BotHookIdRequest = z.infer<typeof BotHookIdRequestSchema>;
|
||||
Reference in New Issue
Block a user