mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix: guard sessions.send/steer against deleted agents (#65524)
This commit is contained in:
committed by
Gustavo Madeira Santana
parent
b87d619072
commit
386623191f
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ErrorCodes } from "../protocol/index.js";
|
||||
import type { GatewayRequestContext, RespondFn } from "./types.js";
|
||||
|
||||
const loadSessionEntryMock = vi.fn();
|
||||
const resolveDeletedAgentIdFromSessionKeyMock = vi.fn();
|
||||
|
||||
vi.mock("../session-utils.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../session-utils.js")>("../session-utils.js");
|
||||
return {
|
||||
...actual,
|
||||
loadSessionEntry: (...args: unknown[]) => loadSessionEntryMock(...args),
|
||||
resolveDeletedAgentIdFromSessionKey: (...args: unknown[]) =>
|
||||
resolveDeletedAgentIdFromSessionKeyMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
import { sessionsHandlers } from "./sessions.js";
|
||||
|
||||
describe("sessions.send / sessions.steer deleted-agent guard", () => {
|
||||
beforeEach(() => {
|
||||
loadSessionEntryMock.mockReset();
|
||||
resolveDeletedAgentIdFromSessionKeyMock.mockReset();
|
||||
});
|
||||
|
||||
for (const method of ["sessions.send", "sessions.steer"] as const) {
|
||||
it(`${method} rejects keys belonging to a deleted agent`, async () => {
|
||||
const orphanKey = "agent:deleted-agent:main";
|
||||
loadSessionEntryMock.mockReturnValue({
|
||||
cfg: {},
|
||||
canonicalKey: orphanKey,
|
||||
storePath: "/tmp/sessions.json",
|
||||
entry: { sessionId: "sess-orphan" },
|
||||
});
|
||||
resolveDeletedAgentIdFromSessionKeyMock.mockReturnValue("deleted-agent");
|
||||
|
||||
const respond = vi.fn() as unknown as RespondFn;
|
||||
const context = {
|
||||
chatAbortControllers: new Map(),
|
||||
broadcastToConnIds: vi.fn(),
|
||||
getSessionEventSubscriberConnIds: () => new Set<string>(),
|
||||
} as unknown as GatewayRequestContext;
|
||||
|
||||
await sessionsHandlers[method]({
|
||||
req: { id: "req-1" } as never,
|
||||
params: { key: orphanKey, message: "hi" },
|
||||
respond,
|
||||
context,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(false, undefined, {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: 'Agent "deleted-agent" no longer exists in configuration',
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
loadSessionEntry,
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionModelRef,
|
||||
@@ -461,7 +462,20 @@ async function handleSessionSend(params: {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const { entry, canonicalKey, storePath } = loadSessionEntry(key);
|
||||
const { cfg, entry, canonicalKey, storePath } = loadSessionEntry(key);
|
||||
// Reject sends/steers targeting sessions whose owning agent was deleted (#65524).
|
||||
const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, canonicalKey);
|
||||
if (deletedAgentId !== null) {
|
||||
params.respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`Agent "${deletedAgentId}" no longer exists in configuration`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!entry?.sessionId) {
|
||||
params.respond(
|
||||
false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
listAgentIds,
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentWorkspaceDir,
|
||||
@@ -41,9 +42,11 @@ 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 {
|
||||
@@ -391,6 +394,25 @@ 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).
|
||||
*/
|
||||
export function resolveDeletedAgentIdFromSessionKey(
|
||||
cfg: OpenClawConfig,
|
||||
sessionKey: string,
|
||||
): string | null {
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
if (agentId === DEFAULT_AGENT_ID) {
|
||||
return null;
|
||||
}
|
||||
if (listAgentIds(cfg).includes(agentId)) {
|
||||
return null;
|
||||
}
|
||||
return agentId;
|
||||
}
|
||||
|
||||
export function loadSessionEntry(sessionKey: string) {
|
||||
const cfg = loadConfig();
|
||||
const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey });
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { listAgentIds } from "../agents/agent-scope.js";
|
||||
import { loadSessionStore, updateSessionStore } from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { DEFAULT_AGENT_ID, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import { parseSessionLabel } from "../sessions/session-label.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
listSessionsFromStore,
|
||||
loadCombinedSessionStoreForGateway,
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
} from "./session-utils.js";
|
||||
|
||||
@@ -40,21 +39,17 @@ function validateSessionAgentExists(
|
||||
cfg: OpenClawConfig,
|
||||
key: string,
|
||||
): SessionsResolveResult | null {
|
||||
const agentId = resolveAgentIdFromSessionKey(key);
|
||||
// DEFAULT_AGENT_ID cannot be deleted and is implied by legacy/global keys.
|
||||
if (agentId === DEFAULT_AGENT_ID) {
|
||||
const deletedAgentId = resolveDeletedAgentIdFromSessionKey(cfg, key);
|
||||
if (deletedAgentId === null) {
|
||||
return null;
|
||||
}
|
||||
if (!listAgentIds(cfg).includes(agentId)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`Agent "${agentId}" no longer exists in configuration`,
|
||||
),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`Agent "${deletedAgentId}" no longer exists in configuration`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function isResolvedSessionKeyVisible(params: {
|
||||
|
||||
Reference in New Issue
Block a user