diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d4c0e5536..566f94f8bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -312,6 +312,8 @@ Docs: https://docs.openclaw.ai - Control UI/models: preserve provider-qualified refs for OpenRouter catalog models whose ids already contain slashes so picker selections submit allowlist-compatible model refs instead of dropping the `openrouter/` prefix. (#63416) Thanks @sallyom. - Plugin SDK/command auth: split command status builders onto the lightweight `openclaw/plugin-sdk/command-status` subpath while preserving deprecated `command-auth` compatibility exports, so auth-only plugin imports no longer pull status/context warmup into CLI onboarding paths. (#63174) Thanks @hxy91819. - Wizard/plugin config: coerce integer-typed plugin config fields from interactive text input so integer schema values persist as numbers instead of failing validation. (#63346) Thanks @jalehman. +- npm packaging: derive required root runtime mirrors from bundled plugin manifests and built root chunks, then install packed release tarballs without the repo `node_modules` so release checks catch missing plugin deps before publish. +- Context engines: reject resolved plugin engines whose reported `info.id` does not match their registered slot id, so malformed engines fail fast before id-based runtime branches can misbehave. (#63222) Thanks @fuller-stack-dev. ## 2026.4.8 diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 51fd031ba1a..f046a0502ca 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -757,6 +757,30 @@ describe("Invalid engine fallback", () => { ); }); + it("rejects resolved engines whose info.id mismatches the registered id", async () => { + const engineId = `mismatched-info-id-${Date.now().toString(36)}`; + registerContextEngine( + engineId, + () => + ({ + info: { id: "legacy", name: "Broken Engine" }, + async ingest() { + return { ingested: false }; + }, + async assemble({ messages }: { messages: AgentMessage[] }) { + return { messages, estimatedTokens: 0 }; + }, + async compact() { + return { ok: true, compacted: false }; + }, + }) as unknown as ContextEngine, + ); + + await expect(resolveContextEngine(configWithSlot(engineId))).rejects.toThrow( + `Context engine "${engineId}" factory returned an invalid ContextEngine: info.id must match registered id "${engineId}".`, + ); + }); + it("rejects resolved engines that omit required lifecycle methods", async () => { const engineId = `invalid-methods-${Date.now().toString(36)}`; registerContextEngine( diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index ec8ffc146f7..f2d65373f9f 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -410,8 +410,11 @@ function describeResolvedContextEngineContractError( issues.push("missing info"); } else { const infoRecord = info as Record; - if (typeof infoRecord.id !== "string" || !infoRecord.id.trim()) { + const infoId = typeof infoRecord.id === "string" ? infoRecord.id.trim() : ""; + if (!infoId) { issues.push("missing info.id"); + } else if (infoId !== engineId) { + issues.push(`info.id must match registered id "${engineId}"`); } if (typeof infoRecord.name !== "string" || !infoRecord.name.trim()) { issues.push("missing info.name");