Let bots manage their hooks, with the user confirming what widens them
Bots get list_hooks, pause_hook and delete_hook, which act directly since they only take away. A new hook, or a new name, brief or filter, or switching a paused one back on, goes through propose_hook: it stores a hook_proposals row and puts a [hook-proposal:ID] card in the reply, which the user it was proposed to confirms or rejects. The card is read from what Confirm saves, with the bot's reason labelled as its own; confirming checks everything again and saves through the Hooks modal's helpers. A new hook's URL is shown on the card once, to the user only, never in the outcome the bot is told. What calls a hook, its computers and its signing are never proposed. Only chat replies propose, and a hook call's own run gets no hook tools, so a caller's payload can't reach them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { z } from "zod";
|
||||
import type { BUN_SQLITE_COBBLE_COMPUTERS } from "@/db/types/db";
|
||||
import { BotHookSources, type BotHookSource } from "@/src/dict/bot-hook-dict";
|
||||
import createHookProposal from "@/src/functions/backend/db/hook-proposals/create-hook-proposal";
|
||||
import grabHookProposalView from "@/src/functions/backend/db/hook-proposals/grab-hook-proposal-view";
|
||||
import hookProposalRef from "@/src/utils/hook-proposal-ref";
|
||||
import type { HarnessTool } from "../../../types";
|
||||
import describeHookProposalForBot from "./describe-hook-proposal-for-bot";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
/**
|
||||
* The conversation it's proposed in, where its card shows.
|
||||
*/
|
||||
session_id: number | string;
|
||||
/**
|
||||
* Where the run is: a new hook runs there too.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The bot's computers where it runs, the current one first.
|
||||
*/
|
||||
computers: BUN_SQLITE_COBBLE_COMPUTERS[];
|
||||
/**
|
||||
* Called with the proposal's ref, so the reply always carries its card.
|
||||
*/
|
||||
onPropose: (params: { ref: string }) => void;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
hook_id?: number;
|
||||
title?: string;
|
||||
instructions?: string;
|
||||
source?: BotHookSource;
|
||||
computer_id?: number;
|
||||
watch_computer_id?: number;
|
||||
watch_filter?: string;
|
||||
enabled?: true;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot propose a hook, or a change to one of its own (name, brief,
|
||||
* filter, switching it back on), for the user to confirm from a card in
|
||||
* the chat. A hook runs its brief on its own whenever something outside
|
||||
* calls it, so the user writes it off, never the bot: nothing is saved
|
||||
* until they confirm, and a new hook's URL is shown to them alone.
|
||||
*/
|
||||
export default function proposeHookTool({
|
||||
user_id,
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
computers,
|
||||
onPropose,
|
||||
}: Params): HarnessTool<Args> {
|
||||
const listed = computers.map((computer) => `${computer.id} (${computer.name || computer.kind})`).join(", ");
|
||||
|
||||
return {
|
||||
name: "propose_hook",
|
||||
description: `Propose a hook for the user to confirm: a secret URL an outside service (a webhook from GitHub, Stripe, a form, a script) calls to give you a task, or a watcher that hands you each browser notification on one of your computers (source browser_notifications, with watch_computer_id and an optional watch_filter, text the notification's site, title or body must contain). Every call queues your instructions (the brief) as a task for you, ${
|
||||
workspace_id ? "in this workspace" : "in your own chat"
|
||||
}, with what the caller sent attached as data, and the result is posted here. Runs carry the brief out without asking the user, so write it as a full brief for a future you with no memory of this chat, saying exactly what to do with each call. ${
|
||||
computers.length ? `Your computers here: ${listed}.` : `You have no computer here, so it can't watch one.`
|
||||
} To change one of your hooks (ids from list_hooks), give hook_id and only what changes: title, instructions, watch_filter, or enabled true to switch a paused one back on; what calls it and where it runs can't change. Nothing is saved until the user presses Confirm on the card. A new hook's URL is shown to the user on the card, once: you never see it, never ask for it, and never set a signing secret (the user adds one from the Hooks menu). Check list_hooks first: if a hook already does the job, propose a change to it instead.`,
|
||||
schema: z.object({
|
||||
hook_id: z.number().int().positive().optional().describe("The hook to change; leave out for a new one"),
|
||||
title: z.string().trim().min(1).max(80).optional().describe(`A short name, e.g. "GitHub pushes"`),
|
||||
instructions: z.string().trim().min(1).max(8000).optional().describe("The brief every call runs"),
|
||||
source: z.enum(BotHookSources.map((s) => s.value) as [BotHookSource, ...BotHookSource[]]).optional(),
|
||||
computer_id: z.number().int().positive().optional().describe("The computer its runs use; leave out for your current one"),
|
||||
watch_computer_id: z.number().int().positive().optional(),
|
||||
watch_filter: z.string().trim().max(300).optional(),
|
||||
enabled: z.literal(true).optional(),
|
||||
reason: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(10, "Say why you propose it")
|
||||
.max(2000)
|
||||
.describe("Why you propose it and what the user asked for, shown on the card as your words"),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const { hook_id, reason, ...data } = args;
|
||||
|
||||
const id = await createHookProposal({
|
||||
user_id,
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
hook_id,
|
||||
data,
|
||||
reason,
|
||||
});
|
||||
|
||||
onPropose({ ref: hookProposalRef({ id }) });
|
||||
|
||||
return describeHookProposalForBot({ view: await grabHookProposalView({ proposal_id: id, user_id }) });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user