fix(cron): prevent isolated hook session-key double-prefixing (land #27333, @MaheshBhushan)

Co-authored-by: MaheshBhushan <mkoduri73@gmail.com>
This commit is contained in:
Peter Steinberger
2026-02-26 12:28:07 +00:00
parent f692288301
commit 8b5ebff67b
3 changed files with 50 additions and 5 deletions

View File

@@ -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.

View File

@@ -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",
);
});
});

View File

@@ -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,
});
}