mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 00:43:57 +00:00
feat: add channel pairing request hook (#97733)
* feat: add channel pairing request hook * fix(plugin-sdk): keep pairing hook types internal * docs: regenerate docs map * docs: regenerate docs map * docs: regenerate docs map * fix(plugin-sdk): keep pairing hook types internal * fix(plugin-sdk): keep pairing hook types internal * docs: refresh plugin hook docs * docs: refresh plugin hook docs * docs: refresh plugin hook docs --------- Co-authored-by: clawSean <260045960+clawSean@users.noreply.github.com> Co-authored-by: Omar Shahine <omarshahine@users.noreply.github.com>
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
ff9754cd53ed77f33d8b4509736286b690675945b90b9a923ee7d4270f504fdd plugin-sdk-api-baseline.json
|
||||
25bc1c31b2f1b3215db392296f5e8b8b41495016428f15eb879fe1fb0df84836 plugin-sdk-api-baseline.jsonl
|
||||
ad3c811b2b11ce21cbad35a1420892789ab2ea9bc3359b246caab7b465fef848 plugin-sdk-api-baseline.json
|
||||
2f4f477911da2a08116b684113aa136ca1d1eebdeb5e2136e191d01cc1c4b07a plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -5495,6 +5495,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- Headings:
|
||||
- H2: Quick start
|
||||
- H2: Hook catalog
|
||||
- H3: Channel pairing requests
|
||||
- H2: Debug runtime hooks
|
||||
- H2: Tool call policy
|
||||
- H3: Exec environment hook
|
||||
|
||||
@@ -128,15 +128,16 @@ observation-only.
|
||||
|
||||
**Messages and delivery**
|
||||
|
||||
| Hook | Purpose |
|
||||
| --------------------------- | ----------------------------------------------------------------- |
|
||||
| **`inbound_claim`** | Claim an inbound message before agent routing (synthetic replies) |
|
||||
| `message_received` | Observe inbound content, sender, thread, and metadata |
|
||||
| **`message_sending`** | Rewrite outbound content or cancel delivery |
|
||||
| **`reply_payload_sending`** | Mutate or cancel normalized reply payloads before delivery |
|
||||
| `message_sent` | Observe outbound delivery success or failure |
|
||||
| **`before_dispatch`** | Inspect or rewrite an outbound dispatch before channel handoff |
|
||||
| **`reply_dispatch`** | Participate in the final reply-dispatch pipeline |
|
||||
| Hook | Purpose |
|
||||
| ------------------------------- | ----------------------------------------------------------------- |
|
||||
| **`inbound_claim`** | Claim an inbound message before agent routing (synthetic replies) |
|
||||
| **`channel_pairing_requested`** | Observe newly created DM pairing requests |
|
||||
| `message_received` | Observe inbound content, sender, thread, and metadata |
|
||||
| **`message_sending`** | Rewrite outbound content or cancel delivery |
|
||||
| **`reply_payload_sending`** | Mutate or cancel normalized reply payloads before delivery |
|
||||
| `message_sent` | Observe outbound delivery success or failure |
|
||||
| **`before_dispatch`** | Inspect or rewrite an outbound dispatch before channel handoff |
|
||||
| **`reply_dispatch`** | Participate in the final reply-dispatch pipeline |
|
||||
|
||||
**Sessions and compaction**
|
||||
|
||||
@@ -163,6 +164,28 @@ observation-only.
|
||||
| `cron_changed` | Observe Gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) |
|
||||
| **`before_install`** | Inspect staged skill or plugin install material from a loaded plugin runtime |
|
||||
|
||||
### Channel pairing requests
|
||||
|
||||
Use `channel_pairing_requested` when a plugin needs to notify an operator or
|
||||
write an audit record after an unpaired DM sender creates a pending pairing
|
||||
request. The hook is dispatched when the request is created; channel delivery of
|
||||
the pairing reply is not delayed by slow or failing hook handlers.
|
||||
|
||||
```typescript
|
||||
api.on("channel_pairing_requested", async (event) => {
|
||||
await notifyOperator({
|
||||
text: `New ${event.channel} pairing request from ${event.senderId}: ${event.code}`,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The hook is observation-only. It does not approve, reject, suppress, or rewrite
|
||||
the pairing reply. The payload includes the channel, optional `accountId`,
|
||||
channel-scoped `senderId`, pairing `code`, and channel metadata. Treat the
|
||||
pairing code as a live single-use approval credential and deliver it only to a
|
||||
trusted operator sink. Treat `metadata` as untrusted sender-supplied identity
|
||||
text. The hook does not include the inbound message body or media.
|
||||
|
||||
## Debug runtime hooks
|
||||
|
||||
Use `before_model_resolve` to switch provider or model for an agent turn - it
|
||||
|
||||
@@ -67,6 +67,7 @@ async function ensureDmComponentAuthorized(params: {
|
||||
}
|
||||
const pairingResult = await createChannelPairingChallengeIssuer({
|
||||
channel: "discord",
|
||||
accountId: ctx.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) => {
|
||||
return await upsertChannelPairingRequest({
|
||||
channel: "discord",
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function handleDiscordDmCommandDecision(params: {
|
||||
const upsertPairingRequest = params.upsertPairingRequest ?? upsertChannelPairingRequest;
|
||||
const result = await createChannelPairingChallengeIssuer({
|
||||
channel: "discord",
|
||||
accountId: params.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertPairingRequest({
|
||||
channel: "discord",
|
||||
|
||||
@@ -1089,6 +1089,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
}
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "imessage",
|
||||
|
||||
@@ -190,6 +190,7 @@ async function sendLinePairingReply(params: {
|
||||
})();
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "line",
|
||||
accountId: context.account.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "line",
|
||||
|
||||
@@ -178,6 +178,7 @@ export async function handleSignalDirectMessageAccess(params: {
|
||||
if (params.dmPolicy === "pairing") {
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "signal",
|
||||
accountId: params.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "signal",
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function authorizeSlackDirectMessage(params: {
|
||||
if (params.ctx.dmPolicy === "pairing") {
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "slack",
|
||||
accountId: params.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "slack",
|
||||
|
||||
@@ -57,6 +57,7 @@ async function issueSmsPairingChallenge(params: {
|
||||
}) {
|
||||
const issueChallenge = createChannelPairingChallengeIssuer({
|
||||
channel: CHANNEL_ID,
|
||||
accountId: params.account.accountId,
|
||||
upsertPairingRequest: async (input) =>
|
||||
await params.channelRuntime.pairing.upsertPairingRequest({
|
||||
channel: CHANNEL_ID,
|
||||
|
||||
@@ -127,6 +127,7 @@ export async function enforceTelegramDmAccess(params: {
|
||||
const telegramUserId = sender.userId ?? sender.candidateId;
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "telegram",
|
||||
accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await (upsertPairingRequest ?? upsertChannelPairingRequest)({
|
||||
channel: "telegram",
|
||||
|
||||
@@ -141,6 +141,7 @@ export async function checkInboundAccessControl(params: {
|
||||
} else {
|
||||
await createChannelPairingChallengeIssuer({
|
||||
channel: "whatsapp",
|
||||
accountId: policy.account.accountId,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "whatsapp",
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// Tests pairing challenge creation, validation, and reply formatting.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
} from "../plugins/hook-runner-global.js";
|
||||
import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
|
||||
import { issuePairingChallenge } from "./pairing-challenge.js";
|
||||
|
||||
describe("issuePairingChallenge", () => {
|
||||
afterEach(() => {
|
||||
resetGlobalHookRunner();
|
||||
});
|
||||
|
||||
function createBaseChallengeParams() {
|
||||
return {
|
||||
channel: "forum",
|
||||
@@ -156,4 +165,79 @@ describe("issuePairingChallenge", () => {
|
||||
] as const)("$name", async ({ setup }) => {
|
||||
await expectIssuedChallengeCase(setup());
|
||||
});
|
||||
|
||||
it("fires channel_pairing_requested only for newly created requests", async () => {
|
||||
const handler = vi.fn(async () => {});
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{
|
||||
hookName: "channel_pairing_requested",
|
||||
handler,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
await issuePairingChallenge({
|
||||
...createBaseChallengeParams(),
|
||||
accountId: "alerts",
|
||||
meta: { username: "alice" },
|
||||
upsertPairingRequest: async () => ({ code: "HOOK1234", created: true }),
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
await issuePairingChallenge({
|
||||
...createBaseChallengeParams(),
|
||||
accountId: "alerts",
|
||||
upsertPairingRequest: async () => ({ code: "EXISTS12", created: false }),
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
{
|
||||
channel: "forum",
|
||||
accountId: "alerts",
|
||||
senderId: "123",
|
||||
code: "HOOK1234",
|
||||
metadata: { username: "alice" },
|
||||
},
|
||||
{
|
||||
channelId: "forum",
|
||||
accountId: "alerts",
|
||||
senderId: "123",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not block pairing replies when pairing-request hooks fail or stall", async () => {
|
||||
const throwingHook = vi.fn(() => {
|
||||
throw new Error("notification failed");
|
||||
});
|
||||
const stallingHook = vi.fn(() => new Promise<void>(() => {}));
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([
|
||||
{
|
||||
hookName: "channel_pairing_requested",
|
||||
handler: throwingHook,
|
||||
pluginId: "throwing",
|
||||
},
|
||||
{
|
||||
hookName: "channel_pairing_requested",
|
||||
handler: stallingHook,
|
||||
pluginId: "stalling",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sendPairingReply = vi.fn(async () => {});
|
||||
|
||||
const result = await issuePairingChallenge({
|
||||
...createBaseChallengeParams(),
|
||||
upsertPairingRequest: async () => ({ code: "FAST1234", created: true }),
|
||||
sendPairingReply,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: true, code: "FAST1234" });
|
||||
expect(throwingHook).toHaveBeenCalledTimes(1);
|
||||
expect(stallingHook).toHaveBeenCalledTimes(1);
|
||||
expect(sendPairingReply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Builds and validates channel pairing challenges for first-time setup.
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import { normalizeAccountId } from "../routing/account-id.js";
|
||||
import { buildPairingReply } from "./pairing-messages.js";
|
||||
|
||||
type PairingMeta = Record<string, string | undefined>;
|
||||
|
||||
export type PairingChallengeParams = {
|
||||
channel: string;
|
||||
accountId?: string;
|
||||
senderId: string;
|
||||
senderIdLine: string;
|
||||
meta?: PairingMeta;
|
||||
@@ -18,6 +21,33 @@ export type PairingChallengeParams = {
|
||||
onReplyError?: (err: unknown) => void;
|
||||
};
|
||||
|
||||
async function runPairingRequestedHook(params: {
|
||||
channel: string;
|
||||
accountId?: string;
|
||||
senderId: string;
|
||||
code: string;
|
||||
meta?: PairingMeta;
|
||||
}): Promise<void> {
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
if (!hookRunner?.hasHooks("channel_pairing_requested")) {
|
||||
return;
|
||||
}
|
||||
await hookRunner.runChannelPairingRequested(
|
||||
{
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
code: params.code,
|
||||
metadata: params.meta,
|
||||
},
|
||||
{
|
||||
channelId: params.channel,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared pairing challenge issuance for DM pairing policy pathways.
|
||||
* Ensures every channel follows the same create-if-missing + reply flow.
|
||||
@@ -33,6 +63,15 @@ export async function issuePairingChallenge(
|
||||
return { created: false };
|
||||
}
|
||||
params.onCreated?.({ code });
|
||||
const accountId = params.accountId ? normalizeAccountId(params.accountId) : undefined;
|
||||
// Notification/audit hooks must not delay the pairing-code reply.
|
||||
void runPairingRequestedHook({
|
||||
channel: params.channel,
|
||||
accountId,
|
||||
senderId: params.senderId,
|
||||
code,
|
||||
meta: params.meta,
|
||||
}).catch(() => undefined);
|
||||
const replyText =
|
||||
params.buildReplyText?.({ code, senderIdLine: params.senderIdLine }) ??
|
||||
buildPairingReply({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/**
|
||||
* Tests channel pairing helpers and pairing reply behavior.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
} from "../plugins/hook-runner-global.js";
|
||||
import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import {
|
||||
createChannelPairingChallengeIssuer,
|
||||
@@ -18,6 +23,10 @@ function createReplyCollector() {
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
resetGlobalHookRunner();
|
||||
});
|
||||
|
||||
describe("createChannelPairingController", () => {
|
||||
it("scopes store access and issues pairing challenges through the scoped store", async () => {
|
||||
const readAllowFromStore = vi.fn(async () => ["alice"]);
|
||||
@@ -58,6 +67,47 @@ describe("createChannelPairingController", () => {
|
||||
expect(sendPairingReply).toHaveBeenCalledTimes(1);
|
||||
expect(replies[0]).toContain("123456");
|
||||
});
|
||||
|
||||
it("passes the scoped account id to channel_pairing_requested hooks", async () => {
|
||||
const handler = vi.fn(async () => {});
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "channel_pairing_requested", handler }]),
|
||||
);
|
||||
const runtime = {
|
||||
channel: {
|
||||
pairing: {
|
||||
readAllowFromStore: vi.fn(async () => []),
|
||||
upsertPairingRequest: vi.fn(async () => ({ code: "ACCT1234", created: true })),
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
const pairing = createChannelPairingController({
|
||||
core: runtime,
|
||||
channel: "googlechat",
|
||||
accountId: "Primary",
|
||||
});
|
||||
|
||||
await pairing.issueChallenge({
|
||||
senderId: "user-1",
|
||||
senderIdLine: "Your id: user-1",
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "googlechat",
|
||||
accountId: "primary",
|
||||
senderId: "user-1",
|
||||
code: "ACCT1234",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
channelId: "googlechat",
|
||||
accountId: "primary",
|
||||
senderId: "user-1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createChannelPairingChallengeIssuer", () => {
|
||||
@@ -81,4 +131,35 @@ describe("createChannelPairingChallengeIssuer", () => {
|
||||
});
|
||||
expect(replies[0]).toContain("654321");
|
||||
});
|
||||
|
||||
it("normalizes account ids before sending channel_pairing_requested hooks", async () => {
|
||||
const handler = vi.fn(async () => {});
|
||||
initializeGlobalHookRunner(
|
||||
createMockPluginRegistry([{ hookName: "channel_pairing_requested", handler }]),
|
||||
);
|
||||
const issueChallenge = createChannelPairingChallengeIssuer({
|
||||
channel: "quietchat",
|
||||
accountId: "Alerts",
|
||||
upsertPairingRequest: vi.fn(async () => ({ code: "NORM1234", created: true })),
|
||||
});
|
||||
|
||||
await issueChallenge({
|
||||
senderId: "user-3",
|
||||
senderIdLine: "Your id: user-3",
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "quietchat",
|
||||
accountId: "alerts",
|
||||
senderId: "user-3",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
channelId: "quietchat",
|
||||
accountId: "alerts",
|
||||
senderId: "user-3",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,10 @@ type ScopedPairingAccess = ReturnType<typeof createScopedPairingAccess>;
|
||||
export type ChannelPairingController = ScopedPairingAccess & {
|
||||
/** Issue a pairing challenge using the controller's channel and scoped store writer. */
|
||||
issueChallenge: (
|
||||
params: Omit<Parameters<typeof issuePairingChallenge>[0], "channel" | "upsertPairingRequest">,
|
||||
params: Omit<
|
||||
Parameters<typeof issuePairingChallenge>[0],
|
||||
"channel" | "accountId" | "upsertPairingRequest"
|
||||
>,
|
||||
) => ReturnType<typeof issuePairingChallenge>;
|
||||
};
|
||||
|
||||
@@ -28,6 +31,8 @@ export type ChannelPairingController = ScopedPairingAccess & {
|
||||
export function createChannelPairingChallengeIssuer(params: {
|
||||
/** Channel id attached to every challenge issued by the returned helper. */
|
||||
channel: ChannelId;
|
||||
/** Optional channel account id attached to pairing-request hook payloads. */
|
||||
accountId?: string;
|
||||
/** Store writer that persists pending pairing requests for the bound channel. */
|
||||
upsertPairingRequest: Parameters<typeof issuePairingChallenge>[0]["upsertPairingRequest"];
|
||||
}) {
|
||||
@@ -35,11 +40,12 @@ export function createChannelPairingChallengeIssuer(params: {
|
||||
/** Challenge details supplied at message handling time. */
|
||||
challenge: Omit<
|
||||
Parameters<typeof issuePairingChallenge>[0],
|
||||
"channel" | "upsertPairingRequest"
|
||||
"channel" | "accountId" | "upsertPairingRequest"
|
||||
>,
|
||||
) =>
|
||||
issuePairingChallenge({
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
upsertPairingRequest: params.upsertPairingRequest,
|
||||
...challenge,
|
||||
});
|
||||
@@ -59,6 +65,7 @@ export function createChannelPairingController(params: {
|
||||
...access,
|
||||
issueChallenge: createChannelPairingChallengeIssuer({
|
||||
channel: params.channel,
|
||||
accountId: access.accountId,
|
||||
upsertPairingRequest: access.upsertPairingRequest,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -94,6 +94,7 @@ export type PluginHookName =
|
||||
| "after_compaction"
|
||||
| "before_reset"
|
||||
| "inbound_claim"
|
||||
| "channel_pairing_requested"
|
||||
| "message_received"
|
||||
| "message_sending"
|
||||
| "reply_payload_sending"
|
||||
@@ -141,6 +142,7 @@ export const PLUGIN_HOOK_NAMES = [
|
||||
"after_compaction",
|
||||
"before_reset",
|
||||
"inbound_claim",
|
||||
"channel_pairing_requested",
|
||||
"message_received",
|
||||
"message_sending",
|
||||
"reply_payload_sending",
|
||||
@@ -180,6 +182,25 @@ export type PluginHookDeprecation = {
|
||||
removeAfter?: string;
|
||||
};
|
||||
|
||||
type PluginHookChannelPairingRequestedEvent = {
|
||||
/** Channel that created the pending pairing request. */
|
||||
channel: string;
|
||||
/** Provider account ID for multi-account channel setups. */
|
||||
accountId?: string;
|
||||
/** Channel-scoped sender ID awaiting operator approval. */
|
||||
senderId: string;
|
||||
/** Short-lived code accepted by `openclaw pairing approve`. */
|
||||
code: string;
|
||||
/** Sender-supplied channel metadata for operator notification/audit. Treat as untrusted. */
|
||||
metadata?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
type PluginHookChannelPairingContext = {
|
||||
channelId: string;
|
||||
accountId?: string;
|
||||
senderId: string;
|
||||
};
|
||||
|
||||
export const DEPRECATED_PLUGIN_HOOKS = {
|
||||
subagent_spawning: {
|
||||
replacement: "`subagent_spawned` for observation; core session bindings for routing",
|
||||
@@ -1144,6 +1165,10 @@ export type PluginHookHandlerMap = {
|
||||
event: PluginHookInboundClaimEvent,
|
||||
ctx: PluginHookInboundClaimContext,
|
||||
) => Promise<PluginHookInboundClaimResult | void> | PluginHookInboundClaimResult | void;
|
||||
channel_pairing_requested: (
|
||||
event: PluginHookChannelPairingRequestedEvent,
|
||||
ctx: PluginHookChannelPairingContext,
|
||||
) => Promise<void> | void;
|
||||
before_dispatch: (
|
||||
event: PluginHookBeforeDispatchEvent,
|
||||
ctx: PluginHookBeforeDispatchContext,
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
PluginHookBeforeDispatchContext,
|
||||
PluginHookBeforeDispatchEvent,
|
||||
PluginHookBeforeDispatchResult,
|
||||
PluginHookHandlerMap,
|
||||
PluginHookReplyPayloadSendingContext,
|
||||
PluginHookReplyPayloadSendingEvent,
|
||||
PluginHookReplyPayloadSendingResult,
|
||||
@@ -206,6 +207,7 @@ export type HookRunnerOptions = {
|
||||
|
||||
const DEFAULT_VOID_HOOK_TIMEOUT_MS_BY_HOOK: Partial<Record<PluginHookName, number>> = {
|
||||
agent_end: 30_000,
|
||||
channel_pairing_requested: 2_000,
|
||||
// Defensive default for the compaction lifecycle hooks. Without a budget an
|
||||
// unresponsive handler runs fully unbounded, and in the codex agent harness
|
||||
// these hooks fire on the serialized notification queue
|
||||
@@ -1064,6 +1066,17 @@ export function createHookRunner(
|
||||
return runVoidHook("message_received", event, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run channel_pairing_requested hook.
|
||||
* Observation-only; slow/failing handlers must not block pairing flow.
|
||||
*/
|
||||
async function runChannelPairingRequested(
|
||||
event: Parameters<PluginHookHandlerMap["channel_pairing_requested"]>[0],
|
||||
ctx: Parameters<PluginHookHandlerMap["channel_pairing_requested"]>[1],
|
||||
): Promise<void> {
|
||||
return runVoidHook("channel_pairing_requested", event, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before_dispatch hook.
|
||||
* Allows plugins to inspect or handle a message before model dispatch.
|
||||
@@ -1643,6 +1656,7 @@ export function createHookRunner(
|
||||
runInboundClaim,
|
||||
runInboundClaimForPlugin,
|
||||
runInboundClaimForPluginOutcome,
|
||||
runChannelPairingRequested,
|
||||
runMessageReceived,
|
||||
runBeforeDispatch,
|
||||
runReplyDispatch,
|
||||
|
||||
Reference in New Issue
Block a user