From 8b5ebff67ba16bb68e26a2a0a22ed9556800009d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 12:28:07 +0000 Subject: [PATCH] fix(cron): prevent isolated hook session-key double-prefixing (land #27333, @MaheshBhushan) Co-authored-by: MaheshBhushan --- CHANGELOG.md | 1 + .../isolated-agent/run.session-key.test.ts | 28 +++++++++++++++++++ src/cron/isolated-agent/run.ts | 26 +++++++++++++---- 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/cron/isolated-agent/run.session-key.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 99e44a32b..a0a2be313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282) - Security/Plugin channel HTTP auth: normalize protected `/api/channels` path checks against canonicalized request paths (case + percent-decoding + slash normalization), and fail closed on malformed `%`-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. - Security/Exec approvals forwarding: prefer turn-source channel/account/thread metadata when resolving approval delivery targets so stale session routes do not misroute approval prompts. - Onboarding/Gateway: seed default Control UI `allowedOrigins` for non-loopback binds during onboarding (`localhost`/`127.0.0.1` plus custom bind host) so fresh non-loopback setups do not fail startup due to missing origin policy. (#26157) thanks @stakeswky. diff --git a/src/cron/isolated-agent/run.session-key.test.ts b/src/cron/isolated-agent/run.session-key.test.ts new file mode 100644 index 000000000..06e9059df --- /dev/null +++ b/src/cron/isolated-agent/run.session-key.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { resolveCronAgentSessionKey } from "./run.js"; + +describe("resolveCronAgentSessionKey", () => { + it("builds an agent-scoped key for legacy aliases", () => { + expect(resolveCronAgentSessionKey({ sessionKey: "main", agentId: "main" })).toBe( + "agent:main:main", + ); + }); + + it("preserves canonical agent keys instead of prefixing twice", () => { + expect(resolveCronAgentSessionKey({ sessionKey: "agent:main:main", agentId: "main" })).toBe( + "agent:main:main", + ); + }); + + it("normalizes canonical keys to lowercase before reuse", () => { + expect( + resolveCronAgentSessionKey({ sessionKey: "AGENT:Main:Hook:Webhook:42", agentId: "x" }), + ).toBe("agent:main:hook:webhook:42"); + }); + + it("keeps hook keys scoped under the target agent", () => { + expect(resolveCronAgentSessionKey({ sessionKey: "hook:webhook:42", agentId: "main" })).toBe( + "agent:main:hook:webhook:42", + ); + }); +}); diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 10b8b5c74..a891ee526 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -40,7 +40,11 @@ import { import type { AgentDefaultsConfig } from "../../config/types.js"; import { registerAgentRunContext } from "../../infra/agent-events.js"; import { logWarn } from "../../logger.js"; -import { buildAgentMainSessionKey, normalizeAgentId } from "../../routing/session-key.js"; +import { + buildAgentMainSessionKey, + normalizeAgentId, + parseAgentSessionKey, +} from "../../routing/session-key.js"; import { buildSafeExternalPrompt, detectSuspiciousPatterns, @@ -142,10 +146,7 @@ export async function runCronIsolatedAgentTurn(params: { }; const baseSessionKey = (params.sessionKey?.trim() || `cron:${params.job.id}`).trim(); - const agentSessionKey = buildAgentMainSessionKey({ - agentId, - mainKey: baseSessionKey, - }); + const agentSessionKey = resolveCronAgentSessionKey({ sessionKey: baseSessionKey, agentId }); const workspaceDirRaw = resolveAgentWorkspaceDir(params.cfg, agentId); const agentDir = resolveAgentDir(params.cfg, agentId); @@ -646,3 +647,18 @@ export async function runCronIsolatedAgentTurn(params: { return resolveRunOutcome({ delivered, deliveryAttempted }); } + +export function resolveCronAgentSessionKey(params: { + sessionKey: string; + agentId: string; +}): string { + const baseSessionKey = params.sessionKey.trim(); + const normalizedBaseSessionKey = baseSessionKey.toLowerCase(); + if (parseAgentSessionKey(normalizedBaseSessionKey)) { + return normalizedBaseSessionKey; + } + return buildAgentMainSessionKey({ + agentId: params.agentId, + mainKey: baseSessionKey, + }); +}