mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix(cron): keep pairing approvals out of automation recipients
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Infer/media: report missing image-understanding and audio-transcription provider configuration for `image describe`, `image describe-many`, and `audio transcribe` instead of blaming the input path when no provider is available. Fixes #73569 and supersedes #73593, #74288, and #74495. Thanks @bittoby, @tmimmanuel, @Linux2010, and @vyctorbrzezowski.
|
||||
- Docs/health: clarify that session listing surfaces stored conversation rows rather than Discord/channel socket liveness, and point connectivity checks at channel status and health probes. Fixes #70420. Thanks @ashersoutherncities-art and @martingarramon.
|
||||
- WhatsApp/Cron: keep DM pairing-store approvals out of implicit cron and heartbeat recipient fallback, so scheduled automation only uses explicit targets, active configured recipients, or configured `allowFrom` entries. Fixes #62339. Thanks @kelvinisly-collab.
|
||||
- Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar.
|
||||
- Plugins/web-provider: reuse the active gateway plugin registry for runtime web provider resolution after deriving the same candidate plugin ids as the loader path, avoiding a redundant `loadOpenClawPlugins` call on every request while preserving origin and scope filters. Fixes #75513. Thanks @jochen.
|
||||
- Crestodian/CLI: exit non-zero when interactive Crestodian is invoked without a TTY, so scripts and CI no longer treat the setup error as success. Fixes #73646 and supersedes #73928 and #74059. Thanks @bittoby, @luyao618, and @Linux2010.
|
||||
|
||||
@@ -164,6 +164,8 @@ For isolated jobs, chat delivery is shared. If a chat route is available, the ag
|
||||
|
||||
When an agent creates an isolated reminder from an active chat, OpenClaw stores the preserved live delivery target for the fallback announce route. Internal session keys may be lowercase; provider delivery targets are not reconstructed from those keys when current chat context is available.
|
||||
|
||||
Implicit announce delivery uses configured channel allowlists to validate and reroute stale targets. DM pairing-store approvals are not fallback automation recipients; set `delivery.to` or configure the channel `allowFrom` entry when a scheduled job should proactively send to a DM.
|
||||
|
||||
Failure notifications follow a separate destination path:
|
||||
|
||||
- `cron.failureDestination` sets a global default for failure notifications.
|
||||
|
||||
@@ -219,6 +219,7 @@ content and identifiers.
|
||||
Runtime behavior details:
|
||||
|
||||
- pairings are persisted in channel allow-store and merged with configured `allowFrom`
|
||||
- scheduled automation and heartbeat recipient fallback use explicit delivery targets or configured `allowFrom`; DM pairing approvals are not implicit cron or heartbeat recipients
|
||||
- if no allowlist is configured, the linked self number is allowed by default
|
||||
- OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send to yourself from the linked device)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
export { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
||||
export { readChannelAllowFromStoreSync } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { normalizeChannelId } from "openclaw/plugin-sdk/channel-targets";
|
||||
export { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
|
||||
@@ -3,12 +3,10 @@ import { resolveWhatsAppHeartbeatRecipients } from "./heartbeat-recipients.js";
|
||||
import type { OpenClawConfig } from "./runtime-api.js";
|
||||
|
||||
const loadSessionStoreMock = vi.hoisted(() => vi.fn());
|
||||
const readChannelAllowFromStoreSyncMock = vi.hoisted(() => vi.fn<() => string[]>(() => []));
|
||||
|
||||
vi.mock("./heartbeat-recipients.runtime.js", () => ({
|
||||
DEFAULT_ACCOUNT_ID: "default",
|
||||
loadSessionStore: loadSessionStoreMock,
|
||||
readChannelAllowFromStoreSync: readChannelAllowFromStoreSyncMock,
|
||||
resolveStorePath: vi.fn(() => "/tmp/test-sessions.json"),
|
||||
normalizeChannelId: (value?: string | null) => {
|
||||
const trimmed = value?.trim().toLowerCase();
|
||||
@@ -36,10 +34,6 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
loadSessionStoreMock.mockReturnValue(store);
|
||||
}
|
||||
|
||||
function setAllowFromStore(entries: string[]) {
|
||||
readChannelAllowFromStoreSyncMock.mockReturnValue(entries);
|
||||
}
|
||||
|
||||
function resolveWith(
|
||||
cfgOverrides: Partial<OpenClawConfig> = {},
|
||||
opts?: Parameters<typeof resolveWhatsAppHeartbeatRecipients>[1],
|
||||
@@ -51,24 +45,22 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000099", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
setAllowFromStore(["+15550000001"]);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
readChannelAllowFromStoreSyncMock.mockReset();
|
||||
loadSessionStoreMock.mockReturnValue({});
|
||||
setAllowFromStore([]);
|
||||
});
|
||||
|
||||
it("uses allowFrom store recipients when session recipients are ambiguous", () => {
|
||||
it("uses configured allowFrom recipients when session recipients are ambiguous", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000001", updatedAt: 2, sessionId: "a" },
|
||||
b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
|
||||
});
|
||||
setAllowFromStore(["+15550000001"]);
|
||||
|
||||
const result = resolveWith();
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001"] } as never },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ recipients: ["+15550000001"], source: "session-single" });
|
||||
});
|
||||
@@ -76,7 +68,9 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
it("falls back to allowFrom when no session recipient is authorized", () => {
|
||||
setSingleUnauthorizedSessionWithAllowFrom();
|
||||
|
||||
const result = resolveWith();
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001"] } as never },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ recipients: ["+15550000001"], source: "allowFrom" });
|
||||
});
|
||||
@@ -84,7 +78,10 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
it("includes both session and allowFrom recipients when --all is set", () => {
|
||||
setSingleUnauthorizedSessionWithAllowFrom();
|
||||
|
||||
const result = resolveWith({}, { all: true });
|
||||
const result = resolveWith(
|
||||
{ channels: { whatsapp: { allowFrom: ["+15550000001"] } as never } },
|
||||
{ all: true },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000099", "+15550000001"],
|
||||
@@ -126,8 +123,9 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
b: { lastChannel: "whatsapp", lastTo: "+15550000002", updatedAt: 1, sessionId: "b" },
|
||||
c: { lastChannel: "whatsapp", lastTo: "+15550000003", updatedAt: 0, sessionId: "c" },
|
||||
});
|
||||
setAllowFromStore(["+15550000001", "+15550000002"]);
|
||||
const result = resolveWith();
|
||||
const result = resolveWith({
|
||||
channels: { whatsapp: { allowFrom: ["+15550000001", "+15550000002"] } as never },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000001", "+15550000002"],
|
||||
source: "session-ambiguous",
|
||||
@@ -145,11 +143,10 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
expect(result).toEqual({ recipients: ["+15550000009"], source: "allowFrom" });
|
||||
});
|
||||
|
||||
it("uses the requested account allowFrom config and pairing store", () => {
|
||||
it("uses the requested account allowFrom config without pairing-store recipients", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
setAllowFromStore(["+15550000002"]);
|
||||
|
||||
const result = resolveWith(
|
||||
{
|
||||
@@ -167,18 +164,16 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
{ accountId: "work" },
|
||||
);
|
||||
|
||||
expect(readChannelAllowFromStoreSyncMock).toHaveBeenCalledWith("whatsapp", process.env, "work");
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000003", "+15550000002"],
|
||||
recipients: ["+15550000003"],
|
||||
source: "allowFrom",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses configured defaultAccount allowFrom config and pairing store when accountId is omitted", () => {
|
||||
it("uses configured defaultAccount allowFrom config when accountId is omitted", () => {
|
||||
setSessionStore({
|
||||
a: { lastChannel: "whatsapp", lastTo: "+15550000077", updatedAt: 2, sessionId: "a" },
|
||||
});
|
||||
setAllowFromStore(["+15550000002"]);
|
||||
|
||||
const result = resolveWith({
|
||||
channels: {
|
||||
@@ -194,9 +189,8 @@ describe("resolveWhatsAppHeartbeatRecipients", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(readChannelAllowFromStoreSyncMock).toHaveBeenCalledWith("whatsapp", process.env, "work");
|
||||
expect(result).toEqual({
|
||||
recipients: ["+15550000003", "+15550000002"],
|
||||
recipients: ["+15550000003"],
|
||||
source: "allowFrom",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
loadSessionStore,
|
||||
normalizeChannelId,
|
||||
normalizeE164,
|
||||
readChannelAllowFromStoreSync,
|
||||
resolveStorePath,
|
||||
type OpenClawConfig,
|
||||
} from "./heartbeat-recipients.runtime.js";
|
||||
@@ -63,14 +62,9 @@ export function resolveWhatsAppHeartbeatRecipients(
|
||||
)
|
||||
.filter((value) => value !== "*")
|
||||
.map(normalizeE164);
|
||||
const storeAllowFrom = readChannelAllowFromStoreSync(
|
||||
"whatsapp",
|
||||
process.env,
|
||||
resolvedAccountId,
|
||||
).map(normalizeE164);
|
||||
|
||||
const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
|
||||
const allowFrom = unique([...configuredAllowFrom, ...storeAllowFrom]);
|
||||
const allowFrom = unique(configuredAllowFrom);
|
||||
|
||||
if (opts.all) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-loaded-read.js";
|
||||
export { readChannelAllowFromStoreEntriesSync } from "../../pairing/allow-from-store-read.js";
|
||||
export { mapAllowFromEntries } from "../../plugin-sdk/channel-config-helpers.js";
|
||||
export { resolveFirstBoundAccountId } from "../../routing/bound-account-read.js";
|
||||
|
||||
@@ -103,6 +103,8 @@ const normalizeTelegramTargetForDeliveryTest = vi.fn((raw: string): string | und
|
||||
beforeEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
normalizeTelegramTargetForDeliveryTest.mockClear();
|
||||
vi.mocked(readChannelAllowFromStoreEntriesSync).mockReset();
|
||||
vi.mocked(readChannelAllowFromStoreEntriesSync).mockReturnValue([]);
|
||||
vi.mocked(resolveOutboundTarget).mockReset();
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
@@ -234,9 +236,8 @@ describe("resolveDeliveryTarget", () => {
|
||||
lastChannel: "alpha",
|
||||
lastTo: "room-denied",
|
||||
});
|
||||
setStoredAlphaAllowFrom(["room-allowed"]);
|
||||
|
||||
const cfg = makeCfg({ bindings: [], channels: { alpha: { allowFrom: [] } } });
|
||||
const cfg = makeCfg({ bindings: [], channels: { alpha: { allowFrom: ["room-allowed"] } } });
|
||||
const result = await resolveLastTarget(cfg);
|
||||
|
||||
expect(result.channel).toBe("alpha");
|
||||
@@ -249,9 +250,8 @@ describe("resolveDeliveryTarget", () => {
|
||||
lastChannel: "alpha",
|
||||
lastTo: "room-denied",
|
||||
});
|
||||
setStoredAlphaAllowFrom(["room-allowed"]);
|
||||
|
||||
const cfg = makeCfg({ bindings: [], channels: { alpha: { allowFrom: [] } } });
|
||||
const cfg = makeCfg({ bindings: [], channels: { alpha: { allowFrom: ["room-allowed"] } } });
|
||||
const result = await resolveDeliveryTarget(
|
||||
cfg,
|
||||
AGENT_ID,
|
||||
@@ -283,6 +283,19 @@ describe("resolveDeliveryTarget", () => {
|
||||
expect(result.to).toBe("room-denied");
|
||||
});
|
||||
|
||||
it("does not use pairing-store entries as implicit automation recipients", async () => {
|
||||
setMainSessionEntry(undefined);
|
||||
setStoredAlphaAllowFrom(["room-paired"]);
|
||||
|
||||
const cfg = makeCfg({ bindings: [], channels: { alpha: { allowFrom: [] } } });
|
||||
const result = await resolveLastTarget(cfg);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.channel).toBe("alpha");
|
||||
expect(result.to).toBeUndefined();
|
||||
expect(readChannelAllowFromStoreEntriesSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to bound accountId when session has no lastAccountId", async () => {
|
||||
setMainSessionEntry(undefined);
|
||||
const cfg = makeForumBoundCfg();
|
||||
|
||||
@@ -241,11 +241,8 @@ export async function resolveDeliveryTarget(
|
||||
|
||||
let effectiveAllowFrom: string[] | undefined;
|
||||
if (mode === "implicit") {
|
||||
const {
|
||||
getLoadedChannelPluginForRead,
|
||||
mapAllowFromEntries,
|
||||
readChannelAllowFromStoreEntriesSync,
|
||||
} = await loadDeliveryTargetRuntime();
|
||||
const { getLoadedChannelPluginForRead, mapAllowFromEntries } =
|
||||
await loadDeliveryTargetRuntime();
|
||||
const channelPlugin = getLoadedChannelPluginForRead(channel);
|
||||
const resolvedAccountId = normalizeAccountId(accountId);
|
||||
const configuredAllowFromRaw = channelPlugin?.config.resolveAllowFrom?.({
|
||||
@@ -255,12 +252,7 @@ export async function resolveDeliveryTarget(
|
||||
const configuredAllowFrom = configuredAllowFromRaw
|
||||
? mapAllowFromEntries(configuredAllowFromRaw)
|
||||
: [];
|
||||
const storeAllowFrom = readChannelAllowFromStoreEntriesSync(
|
||||
channel,
|
||||
process.env,
|
||||
resolvedAccountId,
|
||||
);
|
||||
const allowFromOverride = [...new Set([...configuredAllowFrom, ...storeAllowFrom])];
|
||||
const allowFromOverride = [...new Set(configuredAllowFrom)];
|
||||
effectiveAllowFrom = allowFromOverride;
|
||||
|
||||
if (toCandidate && allowFromOverride.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user