From 53f4397bdbcd39c58e621885e2f8b369ef717920 Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Mon, 11 May 2026 15:58:54 -0700 Subject: [PATCH] fix(plugin): declare OpenClaw compatibility metadata Signed-off-by: Aaron Erickson --- nemoclaw/package.json | 10 +++- nemoclaw/src/index.ts | 10 ++-- nemoclaw/src/package-metadata.test.ts | 27 +++++++++++ nemoclaw/src/register.test.ts | 2 +- nemoclaw/src/runtime-context.test.ts | 67 ++++++++++++++++----------- nemoclaw/src/runtime-context.ts | 4 +- nemoclaw/vitest.config.ts | 11 +++++ 7 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 nemoclaw/src/package-metadata.test.ts create mode 100644 nemoclaw/vitest.config.ts diff --git a/nemoclaw/package.json b/nemoclaw/package.json index 89dfcd6c66d..ef16b8ecb11 100644 --- a/nemoclaw/package.json +++ b/nemoclaw/package.json @@ -9,11 +9,19 @@ "openclaw": { "extensions": [ "./dist/index.js" - ] + ], + "compat": { + "pluginApi": ">=2026.5.10-beta.1", + "minGatewayVersion": "2026.5.10-beta.1" + }, + "build": { + "openclawVersion": "2026.5.10-beta.1" + } }, "scripts": { "build": "tsc", "dev": "tsc --watch", + "test": "vitest run --config vitest.config.ts", "lint": "biome lint src", "lint:fix": "biome lint --write src", "format": "biome format --write src", diff --git a/nemoclaw/src/index.ts b/nemoclaw/src/index.ts index d18030248d4..7beb74202ee 100644 --- a/nemoclaw/src/index.ts +++ b/nemoclaw/src/index.ts @@ -183,13 +183,17 @@ export interface BeforeToolCallResult { blockReason?: string; } -/** Return value from a before_agent_start hook. */ -export interface BeforeAgentStartResult { +/** Return value from a before_prompt_build hook. */ +export interface BeforePromptBuildResult { + systemPrompt?: string; prependContext?: string; + appendContext?: string; + prependSystemContext?: string; + appendSystemContext?: string; } /** Union of all hook result types. */ -export type HookResult = BeforeToolCallResult | BeforeAgentStartResult | undefined; +export type HookResult = BeforeToolCallResult | BeforePromptBuildResult | undefined; /** * The API object injected into the plugin's register function by the OpenClaw diff --git a/nemoclaw/src/package-metadata.test.ts b/nemoclaw/src/package-metadata.test.ts new file mode 100644 index 00000000000..214fa57bd8b --- /dev/null +++ b/nemoclaw/src/package-metadata.test.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const packageJson = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), +) as { + openclaw?: { + compat?: { + pluginApi?: unknown; + minGatewayVersion?: unknown; + }; + build?: { + openclawVersion?: unknown; + }; + }; +}; + +describe("OpenClaw package metadata", () => { + it("declares the required external plugin compatibility fields", () => { + expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.5.10-beta.1"); + expect(packageJson.openclaw?.compat?.minGatewayVersion).toBe("2026.5.10-beta.1"); + expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.10-beta.1"); + }); +}); diff --git a/nemoclaw/src/register.test.ts b/nemoclaw/src/register.test.ts index c43455a8428..d8e14e68419 100644 --- a/nemoclaw/src/register.test.ts +++ b/nemoclaw/src/register.test.ts @@ -65,7 +65,7 @@ describe("plugin registration", () => { it("continues registration when the runtime context hook is unsupported", () => { const api = createMockApi(); vi.mocked(api.on).mockImplementation((hookName: string) => { - if (hookName === "before_agent_start") { + if (hookName === "before_prompt_build") { throw new Error("unsupported hook"); } }); diff --git a/nemoclaw/src/runtime-context.test.ts b/nemoclaw/src/runtime-context.test.ts index e9d0ea1cea2..0ba758cb195 100644 --- a/nemoclaw/src/runtime-context.test.ts +++ b/nemoclaw/src/runtime-context.test.ts @@ -330,11 +330,12 @@ network_policies: {} describe("registerRuntimeContext", () => { describe("hook registration", () => { - it("registers a before_agent_start hook", () => { + it("registers a before_prompt_build hook", () => { const { api } = makeMockApi(); const onSpy = vi.spyOn(api, "on"); registerRuntimeContext(api, defaultConfig); - expect(onSpy).toHaveBeenCalledWith("before_agent_start", expect.any(Function)); + expect(onSpy).toHaveBeenCalledWith("before_prompt_build", expect.any(Function)); + expect(onSpy).not.toHaveBeenCalledWith(`before_${"agent_start"}`, expect.any(Function)); }); }); @@ -342,7 +343,11 @@ describe("registerRuntimeContext", () => { it("returns an object with prependContext", async () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); - const result = await api._trigger("before_agent_start", {}, { sessionKey: nextSessionKey() }); + const result = await api._trigger( + "before_prompt_build", + {}, + { sessionKey: nextSessionKey() }, + ); expect(result).toHaveProperty("prependContext"); }); @@ -350,7 +355,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -364,7 +369,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -377,7 +382,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -390,7 +395,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -403,7 +408,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -416,7 +421,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -431,16 +436,20 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const key = { sessionKey: nextSessionKey() }; - await api._trigger("before_agent_start", {}, key); - const second = await api._trigger("before_agent_start", {}, key); + await api._trigger("before_prompt_build", {}, key); + const second = await api._trigger("before_prompt_build", {}, key); expect(second).toBeUndefined(); }); it("re-injects for a different session key", async () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); - await api._trigger("before_agent_start", {}, { sessionKey: nextSessionKey() }); - const result = await api._trigger("before_agent_start", {}, { sessionKey: nextSessionKey() }); + await api._trigger("before_prompt_build", {}, { sessionKey: nextSessionKey() }); + const result = await api._trigger( + "before_prompt_build", + {}, + { sessionKey: nextSessionKey() }, + ); expect(result).toHaveProperty("prependContext"); }); }); @@ -452,7 +461,7 @@ describe("registerRuntimeContext", () => { const key = { sessionKey: nextSessionKey() }; // First call: Running - await api._trigger("before_agent_start", {}, key); + await api._trigger("before_prompt_build", {}, key); // Phase changes mockExecFile({ @@ -461,7 +470,7 @@ describe("registerRuntimeContext", () => { "policy get openclaw": POLICY_SUMMARY, }); - const result = (await api._trigger("before_agent_start", {}, key)) as { + const result = (await api._trigger("before_prompt_build", {}, key)) as { prependContext: string; }; expect(result.prependContext).toContain(""); @@ -473,7 +482,7 @@ describe("registerRuntimeContext", () => { registerRuntimeContext(api, defaultConfig); const key = { sessionKey: nextSessionKey() }; - await api._trigger("before_agent_start", {}, key); + await api._trigger("before_prompt_build", {}, key); mockExecFile({ "sandbox get openclaw": SANDBOX_RUNNING, @@ -481,7 +490,7 @@ describe("registerRuntimeContext", () => { "policy get openclaw": "Version: 1.1\nHash: newHash999\nStatus: active", }); - const result = await api._trigger("before_agent_start", {}, key); + const result = await api._trigger("before_prompt_build", {}, key); expect(result).toHaveProperty("prependContext"); expect((result as { prependContext: string }).prependContext).toContain( "", @@ -493,7 +502,7 @@ describe("registerRuntimeContext", () => { registerRuntimeContext(api, defaultConfig); const key = { sessionKey: nextSessionKey() }; - await api._trigger("before_agent_start", {}, key); + await api._trigger("before_prompt_build", {}, key); mockExecFile({ "sandbox get openclaw": SANDBOX_STOPPED, @@ -501,7 +510,7 @@ describe("registerRuntimeContext", () => { "policy get openclaw": "Version: 2.0\nHash: new123\nStatus: active", }); - const result = (await api._trigger("before_agent_start", {}, key)) as { + const result = (await api._trigger("before_prompt_build", {}, key)) as { prependContext: string; }; expect(result.prependContext).toContain(""); @@ -518,7 +527,7 @@ describe("registerRuntimeContext", () => { const { api, warnMessages } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -540,7 +549,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); const result = (await api._trigger( - "before_agent_start", + "before_prompt_build", {}, { sessionKey: nextSessionKey() }, )) as { @@ -558,7 +567,11 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); - const result = await api._trigger("before_agent_start", {}, { sessionKey: nextSessionKey() }); + const result = await api._trigger( + "before_prompt_build", + {}, + { sessionKey: nextSessionKey() }, + ); // Null fingerprint fields are handled gracefully — context is still injected expect(result).toHaveProperty("prependContext"); }); @@ -571,8 +584,8 @@ describe("registerRuntimeContext", () => { // Two separate calls with the same session key const sessionKey = "isolated-session-key"; - const first = await api._trigger("before_agent_start", {}, { sessionKey }); - const second = await api._trigger("before_agent_start", {}, { sessionKey }); + const first = await api._trigger("before_prompt_build", {}, { sessionKey }); + const second = await api._trigger("before_prompt_build", {}, { sessionKey }); expect(first).toHaveProperty("prependContext"); expect(second).toBeUndefined(); @@ -582,8 +595,8 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, defaultConfig); - const first = await api._trigger("before_agent_start", {}, {}); - const second = await api._trigger("before_agent_start", {}, {}); + const first = await api._trigger("before_prompt_build", {}, {}); + const second = await api._trigger("before_prompt_build", {}, {}); // Without a session key caching is disabled — both calls must inject context. expect(first).toHaveProperty("prependContext"); @@ -603,7 +616,7 @@ describe("registerRuntimeContext", () => { const { api } = makeMockApi(); registerRuntimeContext(api, cfg); - const result = await api._trigger("before_agent_start", {}, null); + const result = await api._trigger("before_prompt_build", {}, null); expect(result).toHaveProperty("prependContext"); }); }); diff --git a/nemoclaw/src/runtime-context.ts b/nemoclaw/src/runtime-context.ts index f2a6f5a1d07..b9c920580d7 100644 --- a/nemoclaw/src/runtime-context.ts +++ b/nemoclaw/src/runtime-context.ts @@ -464,14 +464,14 @@ export async function getRuntimeSummary(pluginConfig: NemoClawConfig): Promise` + * Registers a `before_prompt_build` hook that prepends a `` * context block (or a `` delta) to each agent turn. * * Falls back to a minimal static context block if openshell is unavailable or * any internal error occurs, and logs a warning via `api.logger`. */ export function registerRuntimeContext(api: OpenClawPluginApi, pluginConfig: NemoClawConfig): void { - api.on("before_agent_start", async (_event: unknown, hookContext: unknown) => { + api.on("before_prompt_build", async (_event: unknown, hookContext: unknown) => { // Initialise to the configured default; overwritten below with the live // state value so the fallback block reflects the active sandbox name even // when the sandbox was changed after the plugin was initialised. diff --git a/nemoclaw/vitest.config.ts b/nemoclaw/vitest.config.ts new file mode 100644 index 00000000000..2b8650af546 --- /dev/null +++ b/nemoclaw/vitest.config.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +});