mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 16:44:45 +00:00
fix(config): sanitize plugin session hydration
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/update: keep shutdown hook-runner imports on a stable dist entry and ship a legacy chunk alias so package swaps do not strand running gateways on missing shutdown chunks. Fixes #81819. Thanks @najef1979-code.
|
||||
- Config persistence: ignore malformed array/scalar auth profile, cron job state, and session store entries instead of hydrating them into numeric profile ids, crashed cron rows, or invalid session records.
|
||||
- Config persistence: strip malformed pending final-delivery session fields on load so replay/recovery paths skip poisoned reply metadata instead of crashing on raw objects.
|
||||
- Config persistence: strip malformed plugin extension state and promoted session-slot ownership on load so corrupted session rows do not leak poisoned plugin metadata into replay/projection paths.
|
||||
- Providers: reject malformed successful Runway, BytePlus, and Ollama embedding responses with provider-owned errors instead of raw parser/type failures, silent bad vectors, or long bogus polling.
|
||||
- Providers/images: reject malformed successful OpenAI-compatible, OpenAI, Google, fal, and OpenRouter image responses with provider-owned errors instead of raw shape failures, silent invalid base64 skips, or empty image results.
|
||||
- Providers/videos: reject malformed successful xAI, OpenRouter, and fal video create, poll, and result responses with provider-owned errors instead of raw parser failures or long bogus polling.
|
||||
|
||||
@@ -410,6 +410,49 @@ describe("session store writer queue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("strips malformed plugin extension state on load", async () => {
|
||||
const { storePath } = await makeTmpStore({
|
||||
"agent:main:plugins": {
|
||||
sessionId: "s-plugins",
|
||||
updatedAt: 100,
|
||||
pluginExtensions: {
|
||||
" workflow-plugin ": {
|
||||
" approval ": { state: "waiting" },
|
||||
scalar: true,
|
||||
huge: "x".repeat(70 * 1024),
|
||||
emptyNamespace: undefined,
|
||||
},
|
||||
"bad-shape": "not-a-namespace-record",
|
||||
"array-shape": [{ workflow: { state: "bad" } }],
|
||||
"": { workflow: { state: "bad" } },
|
||||
},
|
||||
pluginExtensionSlotKeys: {
|
||||
" workflow-plugin ": {
|
||||
" approval ": " approvalSnapshot ",
|
||||
reserved: "sessionId",
|
||||
dotted: "approval.snapshot",
|
||||
empty: "",
|
||||
},
|
||||
"bad-shape": "approvalSnapshot",
|
||||
"": { workflow: "ignoredSlot" },
|
||||
},
|
||||
},
|
||||
} as unknown as Record<string, SessionEntry>);
|
||||
|
||||
const store = loadSessionStore(storePath, { skipCache: true });
|
||||
expect(store["agent:main:plugins"]?.pluginExtensions).toEqual({
|
||||
"workflow-plugin": {
|
||||
approval: { state: "waiting" },
|
||||
scalar: true,
|
||||
},
|
||||
});
|
||||
expect(store["agent:main:plugins"]?.pluginExtensionSlotKeys).toEqual({
|
||||
"workflow-plugin": {
|
||||
approval: "approvalSnapshot",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("skips session store disk writes when payload is unchanged", async () => {
|
||||
const key = "agent:main:no-op-save";
|
||||
const { storePath } = await makeTmpStore({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { isPluginJsonValue, type PluginJsonValue } from "../../plugins/host-hook-json.js";
|
||||
import { normalizeSessionEntrySlotKey } from "../../plugins/session-entry-slot-keys.js";
|
||||
import {
|
||||
normalizeDeliveryContext,
|
||||
normalizeSessionDeliveryFields,
|
||||
@@ -59,6 +61,11 @@ function normalizeOptionalStringOrNull(value: unknown): string | null | undefine
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeRecordKey(value: string): string | undefined {
|
||||
const key = value.trim();
|
||||
return key.length > 0 ? key : undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalDeliveryContext(
|
||||
value: unknown,
|
||||
): SessionEntry["pendingFinalDeliveryContext"] {
|
||||
@@ -138,6 +145,111 @@ function normalizePendingFinalDeliveryFields(entry: SessionEntry): SessionEntry
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizePluginExtensions(entry: SessionEntry): SessionEntry {
|
||||
if (entry.pluginExtensions === undefined) {
|
||||
return entry;
|
||||
}
|
||||
if (!isRecord(entry.pluginExtensions)) {
|
||||
const next = { ...entry };
|
||||
delete next.pluginExtensions;
|
||||
return next;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const normalizedExtensions: Record<string, Record<string, PluginJsonValue>> = {};
|
||||
for (const [rawPluginId, rawPluginState] of Object.entries(entry.pluginExtensions)) {
|
||||
const pluginId = normalizeRecordKey(rawPluginId);
|
||||
if (!pluginId || !isRecord(rawPluginState)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (pluginId !== rawPluginId) {
|
||||
changed = true;
|
||||
}
|
||||
const normalizedPluginState: Record<string, PluginJsonValue> = {};
|
||||
for (const [rawNamespace, rawValue] of Object.entries(rawPluginState)) {
|
||||
const namespace = normalizeRecordKey(rawNamespace);
|
||||
if (!namespace || !isPluginJsonValue(rawValue)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (namespace !== rawNamespace) {
|
||||
changed = true;
|
||||
}
|
||||
normalizedPluginState[namespace] = rawValue;
|
||||
}
|
||||
if (Object.keys(normalizedPluginState).length === 0) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
normalizedExtensions[pluginId] = normalizedPluginState;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return entry;
|
||||
}
|
||||
const next = { ...entry };
|
||||
if (Object.keys(normalizedExtensions).length > 0) {
|
||||
next.pluginExtensions = normalizedExtensions;
|
||||
} else {
|
||||
delete next.pluginExtensions;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizePluginExtensionSlotKeys(entry: SessionEntry): SessionEntry {
|
||||
if (entry.pluginExtensionSlotKeys === undefined) {
|
||||
return entry;
|
||||
}
|
||||
if (!isRecord(entry.pluginExtensionSlotKeys)) {
|
||||
const next = { ...entry };
|
||||
delete next.pluginExtensionSlotKeys;
|
||||
return next;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const normalizedSlotKeys: Record<string, Record<string, string>> = {};
|
||||
for (const [rawPluginId, rawPluginSlots] of Object.entries(entry.pluginExtensionSlotKeys)) {
|
||||
const pluginId = normalizeRecordKey(rawPluginId);
|
||||
if (!pluginId || !isRecord(rawPluginSlots)) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (pluginId !== rawPluginId) {
|
||||
changed = true;
|
||||
}
|
||||
const normalizedPluginSlots: Record<string, string> = {};
|
||||
for (const [rawNamespace, rawSlotKey] of Object.entries(rawPluginSlots)) {
|
||||
const namespace = normalizeRecordKey(rawNamespace);
|
||||
const slotKey = normalizeSessionEntrySlotKey(rawSlotKey);
|
||||
if (!namespace || !slotKey.ok) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if (namespace !== rawNamespace || slotKey.key !== rawSlotKey) {
|
||||
changed = true;
|
||||
}
|
||||
normalizedPluginSlots[namespace] = slotKey.key;
|
||||
}
|
||||
if (Object.keys(normalizedPluginSlots).length === 0) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
normalizedSlotKeys[pluginId] = normalizedPluginSlots;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return entry;
|
||||
}
|
||||
const next = { ...entry };
|
||||
if (Object.keys(normalizedSlotKeys).length > 0) {
|
||||
next.pluginExtensionSlotKeys = normalizedSlotKeys;
|
||||
} else {
|
||||
delete next.pluginExtensionSlotKeys;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry {
|
||||
const normalized = normalizeSessionDeliveryFields({
|
||||
channel: entry.channel,
|
||||
@@ -195,8 +307,12 @@ export function normalizeSessionStore(store: Record<string, SessionEntry>): bool
|
||||
continue;
|
||||
}
|
||||
const normalized = stripPersistedSkillsCache(
|
||||
normalizePendingFinalDeliveryFields(
|
||||
normalizeSessionEntryDelivery(normalizeSessionRuntimeModelFields(entry)),
|
||||
normalizePluginExtensionSlotKeys(
|
||||
normalizePluginExtensions(
|
||||
normalizePendingFinalDeliveryFields(
|
||||
normalizeSessionEntryDelivery(normalizeSessionRuntimeModelFields(entry)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (normalized !== entry) {
|
||||
|
||||
Reference in New Issue
Block a user