Compare commits

...

2 Commits

Author SHA1 Message Date
Vincent Koc
73393b8088
test: include adversarial middleware diagnostic 2026-04-29 13:37:21 -07:00
Vincent Koc
ed106ac203
feat: split kitchen sink plugin personalities 2026-04-29 13:34:43 -07:00
9 changed files with 174 additions and 9 deletions

View File

@ -17,6 +17,16 @@ detached-task, and text-provider catalog surfaces.
It should not call external services, read secrets, spawn processes, or require
live credentials.
The plugin exposes three test personalities through
`plugins.entries.openclaw-kitchen-sink-fixture.config.personality`:
- `full` is the default compatibility mode and keeps both generated probe
registrations and the hand-owned runtime.
- `conformance` loads only the valid runtime surfaces and skips intentionally
invalid probes so OpenClaw can assert a clean external-plugin install.
- `adversarial` loads only generated invalid probes so OpenClaw can assert
expected diagnostics without mixing them with a live runtime smoke.
## Kitchen Runtime
The fixture can be used dry, without an LLM:

View File

@ -197,6 +197,15 @@
"enabled": {
"type": "boolean",
"default": false
},
"personality": {
"type": "string",
"enum": [
"full",
"conformance",
"adversarial"
],
"default": "full"
}
}
}

View File

@ -30,6 +30,7 @@
},
"exports": {
".": "./src/index.js",
"./personality": "./src/personality.js",
"./runtime": "./src/kitchen-runtime.js",
"./scenarios": "./src/scenarios.js",
"./setup": "./src/setup.js"

View File

@ -387,8 +387,88 @@ assert.equal(cliRegistration[1].descriptors[0].name, "kitchen-sink");
const imageTool = findRegistration("registerTool", "kitchen_sink_image_job");
assert.equal(typeof imageTool.execute, "function");
const { KITCHEN_SINK_EXPECTED_DIAGNOSTICS } = await import("../src/personality.js");
assert.deepEqual(KITCHEN_SINK_EXPECTED_DIAGNOSTICS.conformance, []);
assert.ok(
KITCHEN_SINK_EXPECTED_DIAGNOSTICS.adversarial.includes(
'channel "kitchen-sink-channel-probe" registration missing required config helpers',
),
);
assert.ok(
KITCHEN_SINK_EXPECTED_DIAGNOSTICS.full.includes(
"only bundled plugins can register agent tool result middleware",
),
);
const conformance = capturePluginRegistration({ personality: "conformance" });
assert.ok(
conformance.registerCommand?.some(([command]) => command.name === "kitchen"),
"conformance registers usable commands",
);
assert.ok(
conformance.registerChannel?.some(([channel]) => channel.id === "kitchen-sink-channel"),
"conformance registers the usable channel",
);
assert.equal(conformance.registerAgentToolResultMiddleware, undefined);
assert.equal(
conformance.registerChannel?.some(([channel]) => channel.id === "kitchen-sink-channel-probe"),
false,
);
const adversarial = capturePluginRegistration({ personality: "adversarial" });
assert.equal(
adversarial.registerCommand?.some(([command]) => command.name === "kitchen"),
false,
);
assert.equal(
adversarial.registerChannel?.some(([channel]) => channel.id === "kitchen-sink-channel"),
false,
);
assert.ok(
adversarial.registerChannel?.some(([channel]) => channel.id === "kitchen-sink-channel-probe"),
"adversarial registers generated invalid channel probe",
);
assert.ok(
adversarial.registerCompactionProvider?.some(([provider]) => provider.id === "kitchen-sink-compaction-provider"),
"adversarial registers generated invalid compaction probe",
);
console.log("Kitchen runtime OK");
function capturePluginRegistration(config) {
const captured = {};
const captureApi = new Proxy(
{
id: "openclaw-kitchen-sink-fixture",
registrationMode: "full",
config,
logger: console,
},
{
get(target, property) {
if (property in target) {
return target[property];
}
if (property === "on") {
return (...args) => {
captured.on ??= [];
captured.on.push(args);
};
}
if (typeof property !== "string" || !property.startsWith("register")) {
return undefined;
}
return (...args) => {
captured[property] ??= [];
captured[property].push(args);
};
},
},
);
plugin.register(captureApi);
return captured;
}
function findRegistration(method, id) {
const entry = registrations[method]?.map(([value]) => value).find((value) => value?.id === id);
assert.ok(entry, `${method} ${id} registered`);

View File

@ -33,6 +33,7 @@ const requiredFiles = [
"src/index.js",
"src/assets/kitchen_sink_office.png",
"src/kitchen-runtime.js",
"src/personality.js",
"src/scenarios.js",
"src/setup.js",
"src/generated-hooks.js",

View File

@ -130,16 +130,28 @@ function renderRuntimeIndex() {
return `import { registerAllHooks } from "./generated-hooks.js";
import { registerAllRegistrars } from "./generated-registrars.js";
import { registerKitchenSinkRuntime } from "./kitchen-runtime.js";
import {
KITCHEN_SINK_EXPECTED_DIAGNOSTICS,
resolveKitchenSinkPersonality,
} from "./personality.js";
export const plugin = {
id: "openclaw-kitchen-sink-fixture",
name: "OpenClaw Kitchen Sink",
version: "${packageJson.version}",
description: "Credential-free fixture covering OpenClaw plugin API seams.",
expectedDiagnostics: KITCHEN_SINK_EXPECTED_DIAGNOSTICS,
register(api) {
const personality = resolveKitchenSinkPersonality(api);
registerAllHooks(api);
registerAllRegistrars(api);
registerKitchenSinkRuntime(api);
if (personality !== "conformance") {
registerAllRegistrars(api);
}
if (personality !== "adversarial") {
registerKitchenSinkRuntime(api, {
includeAgentToolResultMiddleware: personality !== "conformance",
});
}
},
};
@ -223,6 +235,7 @@ function renderManifest({ manifestContracts, packageVersion }) {
additionalProperties: false,
properties: {
enabled: { type: "boolean", default: false },
personality: { type: "string", enum: ["full", "conformance", "adversarial"], default: "full" },
},
},
};

View File

@ -1,16 +1,28 @@
import { registerAllHooks } from "./generated-hooks.js";
import { registerAllRegistrars } from "./generated-registrars.js";
import { registerKitchenSinkRuntime } from "./kitchen-runtime.js";
import {
KITCHEN_SINK_EXPECTED_DIAGNOSTICS,
resolveKitchenSinkPersonality,
} from "./personality.js";
export const plugin = {
id: "openclaw-kitchen-sink-fixture",
name: "OpenClaw Kitchen Sink",
version: "0.2.1",
description: "Credential-free fixture covering OpenClaw plugin API seams.",
expectedDiagnostics: KITCHEN_SINK_EXPECTED_DIAGNOSTICS,
register(api) {
const personality = resolveKitchenSinkPersonality(api);
registerAllHooks(api);
registerAllRegistrars(api);
registerKitchenSinkRuntime(api);
if (personality !== "conformance") {
registerAllRegistrars(api);
}
if (personality !== "adversarial") {
registerKitchenSinkRuntime(api, {
includeAgentToolResultMiddleware: personality !== "conformance",
});
}
},
};

View File

@ -53,6 +53,7 @@ export { createKitchenSinkImageAsset, kitchenPromptGuidance, shouldHandleKitchen
export function registerKitchenSinkRuntime(api, options = {}) {
const runtime = createKitchenSinkRuntime(options);
const includeAgentToolResultMiddleware = options.includeAgentToolResultMiddleware !== false;
optionalRegister(api, "registerCommand", () => api.registerCommand(buildKitchenCommand(runtime)));
optionalRegister(api, "registerCommand", () => api.registerCommand(buildKitchenSinkCommand(runtime)));
@ -101,11 +102,13 @@ export function registerKitchenSinkRuntime(api, options = {}) {
optionalRegister(api, "registerCompactionProvider", () =>
api.registerCompactionProvider(buildKitchenCompactionProvider()),
);
optionalRegister(api, "registerAgentToolResultMiddleware", () =>
api.registerAgentToolResultMiddleware(buildKitchenToolResultMiddleware(), {
runtimes: ["pi", "codex", "cli"],
}),
);
if (includeAgentToolResultMiddleware) {
optionalRegister(api, "registerAgentToolResultMiddleware", () =>
api.registerAgentToolResultMiddleware(buildKitchenToolResultMiddleware(), {
runtimes: ["pi", "codex", "cli"],
}),
);
}
optionalRegister(api, "registerService", () => api.registerService(buildKitchenService()));
optionalRegister(api, "registerHttpRoute", () => api.registerHttpRoute(buildKitchenHttpRoute()));
optionalRegister(api, "registerGatewayMethod", () =>

36
src/personality.js Normal file
View File

@ -0,0 +1,36 @@
export const KITCHEN_SINK_PERSONALITIES = ["full", "conformance", "adversarial"];
export const DEFAULT_KITCHEN_SINK_PERSONALITY = "full";
export const KITCHEN_SINK_EXPECTED_DIAGNOSTICS = {
full: [
"only bundled plugins can register agent tool result middleware",
'agent harness "kitchen-sink-agent-harness" registration missing required runtime methods',
'channel "kitchen-sink-channel-probe" registration missing required config helpers',
"cli registration missing explicit commands metadata",
"only bundled plugins can register Codex app-server extension factories",
'compaction provider "kitchen-sink-compaction-provider" registration missing summarize',
"context engine registration missing id",
"http route registration missing or invalid auth: /kitchen-sink/http-route",
"plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider",
"memory prompt supplement registration missing builder",
],
conformance: [],
adversarial: [
"only bundled plugins can register agent tool result middleware",
'agent harness "kitchen-sink-agent-harness" registration missing required runtime methods',
'channel "kitchen-sink-channel-probe" registration missing required config helpers',
"cli registration missing explicit commands metadata",
"only bundled plugins can register Codex app-server extension factories",
'compaction provider "kitchen-sink-compaction-provider" registration missing summarize',
"context engine registration missing id",
"http route registration missing or invalid auth: /kitchen-sink/http-route",
"plugin must own memory slot or declare contracts.memoryEmbeddingProviders for adapter: kitchen-sink-memory-embedding-provider",
"memory prompt supplement registration missing builder",
],
};
export function resolveKitchenSinkPersonality(api) {
const configured = api?.config?.personality || process.env.OPENCLAW_KITCHEN_SINK_PERSONALITY;
return KITCHEN_SINK_PERSONALITIES.includes(configured) ? configured : DEFAULT_KITCHEN_SINK_PERSONALITY;
}