mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix: guard deleted main-agent sessions
This commit is contained in:
@@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Active Memory: raise the blocking recall timeout ceiling to 120 seconds and reject larger config values during plugin schema validation. Fixes #68410. (#68480) Thanks @Bartok9.
|
||||
- Control UI/chat: keep history-backed user image uploads visible after chat reload while filtering blocked or non-image transcript media paths. (#68415) Thanks @mraleko.
|
||||
- Matrix/plugins: keep remaining Matrix event helpers on the canonical `matrix-js-sdk` subpath so build and plugin-load entrypoint checks stay consistent. (#68498) Thanks @masatohoshino.
|
||||
- Gateway/sessions: reject stale agent-scoped sessions after an agent is removed from config while preserving legacy default-agent main-session aliases. (#65986) Thanks @bittoby.
|
||||
|
||||
## 2026.4.15
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import {
|
||||
canonicalizeMainSessionAlias,
|
||||
resolveMainSessionKey,
|
||||
} from "../config/sessions/main-session.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
normalizeAgentId,
|
||||
normalizeMainKey,
|
||||
parseAgentSessionKey,
|
||||
type ParsedAgentSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -29,6 +31,22 @@ function resolveDefaultStoreAgentId(cfg: OpenClawConfig): string {
|
||||
return normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||
}
|
||||
|
||||
function resolveParsedSessionStoreAgentId(
|
||||
cfg: OpenClawConfig,
|
||||
parsed: ParsedAgentSessionKey,
|
||||
): string {
|
||||
const agentId = normalizeAgentId(parsed.agentId);
|
||||
if (agentId !== DEFAULT_AGENT_ID || listAgentIds(cfg).includes(DEFAULT_AGENT_ID)) {
|
||||
return agentId;
|
||||
}
|
||||
const rest = normalizeLowercaseStringOrEmpty(parsed.rest);
|
||||
const mainKey = normalizeMainKey(cfg.session?.mainKey);
|
||||
if (rest === "main" || rest === mainKey) {
|
||||
return resolveDefaultStoreAgentId(cfg);
|
||||
}
|
||||
return agentId;
|
||||
}
|
||||
|
||||
export function resolveSessionStoreKey(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
@@ -44,8 +62,12 @@ export function resolveSessionStoreKey(params: {
|
||||
|
||||
const parsed = parseAgentSessionKey(raw);
|
||||
if (parsed) {
|
||||
const agentId = normalizeAgentId(parsed.agentId);
|
||||
const lowered = normalizeLowercaseStringOrEmpty(raw);
|
||||
const originalAgentId = normalizeAgentId(parsed.agentId);
|
||||
const agentId = resolveParsedSessionStoreAgentId(params.cfg, parsed);
|
||||
const lowered =
|
||||
agentId === originalAgentId
|
||||
? normalizeLowercaseStringOrEmpty(raw)
|
||||
: `agent:${agentId}:${normalizeLowercaseStringOrEmpty(parsed.rest)}`;
|
||||
const canonical = canonicalizeMainSessionAlias({
|
||||
cfg: params.cfg,
|
||||
agentId,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
parseGroupKey,
|
||||
pruneLegacyStoreKeys,
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveGatewayModelSupportsImages,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionModelIdentityRef,
|
||||
@@ -105,9 +106,36 @@ describe("gateway session utils", () => {
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "work" })).toBe("agent:ops:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:ops:main" })).toBe("agent:ops:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:ops:MAIN" })).toBe("agent:ops:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:main:main" })).toBe("agent:ops:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:main:work" })).toBe("agent:ops:work");
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "MAIN" })).toBe("agent:ops:work");
|
||||
});
|
||||
|
||||
test("resolveSessionStoreKey preserves non-alias agent:main keys for deleted-agent checks", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:main:discord:direct:u1" })).toBe(
|
||||
"agent:main:discord:direct:u1",
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveDeletedAgentIdFromSessionKey rejects non-alias main keys when main is absent", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "work" },
|
||||
agents: { list: [{ id: "ops", default: true }] },
|
||||
} as OpenClawConfig;
|
||||
const legacyMainAlias = resolveSessionStoreKey({ cfg, sessionKey: "agent:main:main" });
|
||||
|
||||
expect(legacyMainAlias).toBe("agent:ops:work");
|
||||
expect(resolveDeletedAgentIdFromSessionKey(cfg, legacyMainAlias)).toBeNull();
|
||||
expect(resolveDeletedAgentIdFromSessionKey(cfg, "global")).toBeNull();
|
||||
expect(resolveDeletedAgentIdFromSessionKey(cfg, "unknown")).toBeNull();
|
||||
expect(resolveDeletedAgentIdFromSessionKey(cfg, "main")).toBeNull();
|
||||
expect(resolveDeletedAgentIdFromSessionKey(cfg, "agent:main:discord:direct:u1")).toBe("main");
|
||||
});
|
||||
|
||||
test("resolveSessionStoreKey canonicalizes bare keys to default agent", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
|
||||
@@ -42,11 +42,9 @@ import {
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
normalizeAgentId,
|
||||
normalizeMainKey,
|
||||
parseAgentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import { isCronRunSessionKey } from "../sessions/session-key-utils.js";
|
||||
import {
|
||||
@@ -396,17 +394,18 @@ function resolveTranscriptUsageFallback(params: {
|
||||
|
||||
/**
|
||||
* Returns the owning agent id if the session key belongs to an agent that is no
|
||||
* longer present in config (deleted). Returns null for legacy/global keys, the
|
||||
* default agent, or when the owning agent still exists (#65524).
|
||||
* longer present in config (deleted). Returns null for non-agent legacy/global
|
||||
* keys, or when the owning agent still exists (#65524).
|
||||
*/
|
||||
export function resolveDeletedAgentIdFromSessionKey(
|
||||
cfg: OpenClawConfig,
|
||||
sessionKey: string,
|
||||
): string | null {
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
if (agentId === DEFAULT_AGENT_ID) {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const agentId = normalizeAgentId(parsed.agentId);
|
||||
if (listAgentIds(cfg).includes(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,32 @@ describe("resolveSessionKeyFromResolveParams", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects non-alias agent:main sessions when main is no longer configured", async () => {
|
||||
const staleMainKey = "agent:main:discord:direct:u1";
|
||||
hoisted.resolveGatewaySessionStoreTargetMock.mockReturnValue({
|
||||
canonicalKey: staleMainKey,
|
||||
storeKeys: [staleMainKey],
|
||||
storePath,
|
||||
});
|
||||
hoisted.loadSessionStoreMock.mockReturnValue({
|
||||
[staleMainKey]: { sessionId: "sess-stale-main", updatedAt: 1 },
|
||||
});
|
||||
hoisted.listAgentIdsMock.mockReturnValue(["ops"]);
|
||||
|
||||
const result = await resolveSessionKeyFromResolveParams({
|
||||
cfg: { agents: { list: [{ id: "ops", default: true }] } },
|
||||
p: { key: staleMainKey },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: 'Agent "main" no longer exists in configuration',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects sessions belonging to a deleted agent (sessionId-based lookup)", async () => {
|
||||
const deletedAgentKey = "agent:deleted-agent:main";
|
||||
hoisted.loadCombinedSessionStoreForGatewayMock.mockReturnValue({
|
||||
|
||||
Reference in New Issue
Block a user