Skip to content

Commit 39eeeb4

Browse files
committed
feat: modernize plugin entry compatibility (#18)
1 parent 39d0665 commit 39eeeb4

File tree

5 files changed

+186
-1
lines changed

5 files changed

+186
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This plugin integrates OpenClaw with [Agent Control](https://github.com/agentcon
2525

2626
> [!WARNING]
2727
> Experimental plugin: this may break across OpenClaw updates. Use in non-production or pinned environments.
28+
>
29+
> The package exports a modern `definePluginEntry(...)` descriptor when the host SDK exposes it, and falls back to the legacy raw `register(api)` export on older gateways.
2830
2931
## Why use this?
3032

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default } from "./src/agent-control-plugin.ts";
1+
export { default } from "./src/plugin-entry.ts";

src/plugin-entry.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { createRequire } from "node:module";
2+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
3+
import register from "./agent-control-plugin.ts";
4+
5+
export const AGENT_CONTROL_PLUGIN_ID = "agent-control-openclaw-plugin";
6+
export const AGENT_CONTROL_PLUGIN_NAME = "Agent Control";
7+
export const AGENT_CONTROL_PLUGIN_DESCRIPTION =
8+
"Registers OpenClaw tools with Agent Control and blocks unsafe tool invocations.";
9+
10+
export type AgentControlPluginEntry = {
11+
id: string;
12+
name: string;
13+
description: string;
14+
register(api: OpenClawPluginApi): void;
15+
};
16+
17+
type DefinePluginEntry = (entry: AgentControlPluginEntry) => AgentControlPluginEntry;
18+
type RequireLike = (specifier: string) => unknown;
19+
type CreateRequireLike = (path: string | URL) => RequireLike;
20+
21+
export const agentControlPluginEntry: AgentControlPluginEntry = {
22+
id: AGENT_CONTROL_PLUGIN_ID,
23+
name: AGENT_CONTROL_PLUGIN_NAME,
24+
description: AGENT_CONTROL_PLUGIN_DESCRIPTION,
25+
register,
26+
};
27+
28+
function tryReadDefinePluginEntryFromModule(
29+
requireFn: RequireLike,
30+
specifier: string,
31+
): DefinePluginEntry | null {
32+
try {
33+
const loaded = requireFn(specifier) as { definePluginEntry?: unknown };
34+
return typeof loaded.definePluginEntry === "function"
35+
? (loaded.definePluginEntry as DefinePluginEntry)
36+
: null;
37+
} catch {
38+
return null;
39+
}
40+
}
41+
42+
export function loadDefinePluginEntry(
43+
createRequireImpl: CreateRequireLike = createRequire as CreateRequireLike,
44+
): DefinePluginEntry | null {
45+
const requireFn = createRequireImpl(import.meta.url);
46+
47+
// Prefer the dedicated modern helper module when it exists, but also accept
48+
// the helper from core because some gateways exposed it there during the
49+
// migration window.
50+
return (
51+
tryReadDefinePluginEntryFromModule(requireFn, "openclaw/plugin-sdk/plugin-entry") ??
52+
tryReadDefinePluginEntryFromModule(requireFn, "openclaw/plugin-sdk/core")
53+
);
54+
}
55+
56+
export function createPluginEntry(
57+
entry: AgentControlPluginEntry,
58+
definePluginEntry: DefinePluginEntry | null,
59+
) {
60+
return definePluginEntry ? definePluginEntry(entry) : entry.register;
61+
}
62+
63+
const definePluginEntry = loadDefinePluginEntry();
64+
65+
export default createPluginEntry(agentControlPluginEntry, definePluginEntry);

test/plugin-entry.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import {
3+
createPluginEntry,
4+
loadDefinePluginEntry,
5+
type AgentControlPluginEntry,
6+
} from "../src/plugin-entry.ts";
7+
8+
describe("plugin entry compatibility", () => {
9+
it("returns the legacy register function when no modern helper is available", () => {
10+
// Given a plugin entry and a gateway without definePluginEntry support
11+
const legacyRegister = vi.fn();
12+
const entry: AgentControlPluginEntry = {
13+
id: "agent-control-openclaw-plugin",
14+
name: "Agent Control",
15+
description: "test entry",
16+
register: legacyRegister,
17+
};
18+
19+
// When the plugin entry is created without a modern helper
20+
const resolved = createPluginEntry(entry, null);
21+
22+
// Then the legacy raw register function is exported
23+
expect(resolved).toBe(legacyRegister);
24+
});
25+
26+
it("wraps the entry with definePluginEntry when the helper is available", () => {
27+
// Given a plugin entry and a gateway that exposes definePluginEntry
28+
const legacyRegister = vi.fn();
29+
const entry: AgentControlPluginEntry = {
30+
id: "agent-control-openclaw-plugin",
31+
name: "Agent Control",
32+
description: "test entry",
33+
register: legacyRegister,
34+
};
35+
const definePluginEntry = vi.fn((value: AgentControlPluginEntry) => ({
36+
...value,
37+
wrapped: true,
38+
}));
39+
40+
// When the plugin entry is created with the helper
41+
const resolved = createPluginEntry(entry, definePluginEntry);
42+
43+
// Then the modern helper receives the descriptor and its result is exported
44+
expect(definePluginEntry).toHaveBeenCalledWith(entry);
45+
expect(resolved).toEqual({
46+
...entry,
47+
wrapped: true,
48+
});
49+
});
50+
51+
it("prefers the dedicated plugin-entry module when it is present", () => {
52+
// Given a createRequire implementation that exposes the dedicated helper module
53+
const defineFromPluginEntry = vi.fn();
54+
const createRequireImpl = vi.fn(() =>
55+
vi.fn((specifier: string) => {
56+
if (specifier === "openclaw/plugin-sdk/plugin-entry") {
57+
return { definePluginEntry: defineFromPluginEntry };
58+
}
59+
throw new Error(`unexpected module lookup: ${specifier}`);
60+
}),
61+
);
62+
63+
// When definePluginEntry is loaded from the gateway SDK
64+
const loaded = loadDefinePluginEntry(createRequireImpl);
65+
66+
// Then the dedicated helper is returned without probing fallback modules
67+
expect(loaded).toBe(defineFromPluginEntry);
68+
});
69+
70+
it("falls back to the core helper during the SDK migration window", () => {
71+
// Given a createRequire implementation without the dedicated helper module
72+
const defineFromCore = vi.fn();
73+
const requireFn = vi.fn((specifier: string) => {
74+
if (specifier === "openclaw/plugin-sdk/plugin-entry") {
75+
throw new Error("module not found");
76+
}
77+
if (specifier === "openclaw/plugin-sdk/core") {
78+
return { definePluginEntry: defineFromCore };
79+
}
80+
throw new Error(`unexpected module lookup: ${specifier}`);
81+
});
82+
83+
// When definePluginEntry is loaded from the gateway SDK
84+
const loaded = loadDefinePluginEntry(vi.fn(() => requireFn));
85+
86+
// Then the core-exported helper is used as a migration fallback
87+
expect(loaded).toBe(defineFromCore);
88+
expect(requireFn).toHaveBeenCalledWith("openclaw/plugin-sdk/plugin-entry");
89+
expect(requireFn).toHaveBeenCalledWith("openclaw/plugin-sdk/core");
90+
});
91+
92+
it("returns null when neither modern helper is available", () => {
93+
// Given a createRequire implementation where both helper lookups fail
94+
const requireFn = vi.fn(() => {
95+
throw new Error("module not found");
96+
});
97+
98+
// When definePluginEntry is loaded from the gateway SDK
99+
const loaded = loadDefinePluginEntry(vi.fn(() => requireFn));
100+
101+
// Then the plugin can fall back to the legacy raw register export
102+
expect(loaded).toBeNull();
103+
});
104+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
declare module "openclaw/plugin-sdk/plugin-entry" {
2+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
3+
4+
export type OpenClawDefinedPluginEntry = {
5+
id: string;
6+
name: string;
7+
description?: string;
8+
register(api: OpenClawPluginApi): unknown;
9+
};
10+
11+
export function definePluginEntry<TEntry extends OpenClawDefinedPluginEntry>(
12+
entry: TEntry,
13+
): TEntry;
14+
}

0 commit comments

Comments
 (0)