Skip to content

Commit 6f5fdb1

Browse files
committed
fix(gateway): validate plugin descriptors and compact refresh
1 parent 0f18e82 commit 6f5fdb1

5 files changed

Lines changed: 128 additions & 6 deletions

File tree

packages/gateway-protocol/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ import {
210210
PluginsSessionActionParamsSchema,
211211
PluginsSessionActionResultSchema,
212212
type PluginsUiDescriptorsParams,
213+
type PluginsUiDescriptorsResult,
213214
PluginsUiDescriptorsParamsSchema,
215+
PluginsUiDescriptorsResultSchema,
214216
ErrorCodes,
215217
type EnvironmentSummary,
216218
EnvironmentSummarySchema,
@@ -880,6 +882,9 @@ export const validatePluginApprovalResolveParams = lazyCompile<PluginApprovalRes
880882
export const validatePluginsUiDescriptorsParams = lazyCompile<PluginsUiDescriptorsParams>(
881883
PluginsUiDescriptorsParamsSchema,
882884
);
885+
export const validatePluginsUiDescriptorsResult = lazyCompile<PluginsUiDescriptorsResult>(
886+
PluginsUiDescriptorsResultSchema,
887+
);
883888
export const validatePluginsSessionActionParams = lazyCompile<PluginsSessionActionParams>(
884889
PluginsSessionActionParamsSchema,
885890
);
@@ -1133,6 +1138,7 @@ export {
11331138
PluginsSessionActionParamsSchema,
11341139
PluginsSessionActionResultSchema,
11351140
PluginsUiDescriptorsParamsSchema,
1141+
PluginsUiDescriptorsResultSchema,
11361142
ModelsListParamsSchema,
11371143
SkillsStatusParamsSchema,
11381144
ToolsCatalogParamsSchema,

src/config/sessions/session-accessor.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,10 @@ describe("session accessor file-backed seam", () => {
993993
expect(updatedEntry?.outputTokens).toBeUndefined();
994994
expect(updatedEntry?.totalTokens).toBeUndefined();
995995
expect(updatedEntry?.totalTokensFresh).toBeUndefined();
996-
expect(updates).toEqual([{ sessionFile: archived }]);
996+
expect(updates).toEqual([
997+
{ sessionFile: archived },
998+
{ sessionFile: fs.realpathSync(manualTranscriptPath) },
999+
]);
9971000
});
9981001

9991002
it("keeps retained messages reachable through an out-of-window label", async () => {

src/config/sessions/session-accessor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,7 @@ async function replaceTranscriptForManualCompact(
11481148
throw err;
11491149
}
11501150
emitSessionTranscriptUpdate({ sessionFile: archived });
1151+
emitSessionTranscriptUpdate({ sessionFile: filePath });
11511152
return archived;
11521153
}
11531154

src/gateway/server-methods/plugin-host-hooks.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
validatePluginsSessionActionParams,
1010
validatePluginsSessionActionResult,
1111
validatePluginsUiDescriptorsParams,
12+
validatePluginsUiDescriptorsResult,
1213
} from "../../../packages/gateway-protocol/src/index.js";
1314
import { formatErrorMessage } from "../../infra/errors.js";
1415
import { createSubsystemLogger } from "../../logging/subsystem.js";
@@ -54,13 +55,44 @@ export const pluginHostHookHandlers: GatewayRequestHandlers = {
5455
);
5556
return;
5657
}
57-
const descriptors = (getActivePluginRegistry()?.controlUiDescriptors ?? []).map((entry) =>
58-
Object.assign({}, entry.descriptor, {
58+
const descriptors = (getActivePluginRegistry()?.controlUiDescriptors ?? []).map((entry) => {
59+
const descriptor: Record<string, unknown> = {
60+
id: entry.descriptor.id,
5961
pluginId: entry.pluginId,
6062
pluginName: entry.pluginName,
61-
}),
62-
);
63-
respond(true, { ok: true, descriptors }, undefined);
63+
surface: entry.descriptor.surface,
64+
label: entry.descriptor.label,
65+
};
66+
if (entry.descriptor.description !== undefined) {
67+
descriptor.description = entry.descriptor.description;
68+
}
69+
if (entry.descriptor.placement !== undefined) {
70+
descriptor.placement = entry.descriptor.placement;
71+
}
72+
if (entry.descriptor.schema !== undefined) {
73+
descriptor.schema = entry.descriptor.schema;
74+
}
75+
if (entry.descriptor.requiredScopes !== undefined) {
76+
descriptor.requiredScopes = entry.descriptor.requiredScopes;
77+
}
78+
return descriptor;
79+
});
80+
const result = { ok: true, descriptors };
81+
if (!validatePluginsUiDescriptorsResult(result)) {
82+
log.warn("invalid plugins.uiDescriptors result", {
83+
errors: validatePluginsUiDescriptorsResult.errors,
84+
});
85+
respond(
86+
false,
87+
undefined,
88+
errorShape(
89+
ErrorCodes.UNAVAILABLE,
90+
`invalid plugins.uiDescriptors result: ${formatValidationErrors(validatePluginsUiDescriptorsResult.errors)}`,
91+
),
92+
);
93+
return;
94+
}
95+
respond(true, result, undefined);
6496
},
6597
"plugins.sessionAction": async ({ params, client, respond }) => {
6698
if (!validatePluginsSessionActionParams(params)) {

src/plugins/contracts/host-hooks.contract.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
} from "openclaw/plugin-sdk/plugin-test-contracts";
88
import { afterEach, describe, expect, it } from "vitest";
99
import {
10+
validatePluginsUiDescriptorsResult,
1011
validatePluginsUiDescriptorsParams,
1112
validateSessionsPluginPatchParams,
1213
} from "../../../packages/gateway-protocol/src/index.js";
1314
import { loadSessionStore, updateSessionStore, type SessionEntry } from "../../config/sessions.js";
1415
import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../../gateway/operator-scopes.js";
16+
import { pluginHostHookHandlers } from "../../gateway/server-methods/plugin-host-hooks.js";
1517
import { buildGatewaySessionRow } from "../../gateway/session-utils.js";
1618
import { withTempConfig } from "../../gateway/test-temp-config.js";
1719
import { emitAgentEvent, resetAgentEventsForTest } from "../../infra/agent-events.js";
@@ -1892,6 +1894,84 @@ describe("host-hook fixture plugin contract", () => {
18921894
).toBe(false);
18931895
expect(validatePluginsUiDescriptorsParams({})).toBe(true);
18941896
expect(validatePluginsUiDescriptorsParams({ pluginId: "host-hook-fixture" })).toBe(false);
1897+
expect(
1898+
validatePluginsUiDescriptorsResult({
1899+
ok: true,
1900+
descriptors: [
1901+
{
1902+
id: "approval-panel",
1903+
pluginId: "host-hook-fixture",
1904+
surface: "session",
1905+
label: "Approval panel",
1906+
},
1907+
],
1908+
}),
1909+
).toBe(true);
1910+
expect(
1911+
validatePluginsUiDescriptorsResult({
1912+
ok: true,
1913+
descriptors: [
1914+
{
1915+
id: "approval-panel",
1916+
pluginId: "host-hook-fixture",
1917+
surface: "session",
1918+
label: "Approval panel",
1919+
leakedRegistryField: true,
1920+
},
1921+
],
1922+
}),
1923+
).toBe(false);
1924+
});
1925+
1926+
it("projects plugin UI descriptors through the strict gateway result shape", () => {
1927+
const { config, registry } = createPluginRegistryFixture();
1928+
registerTestPlugin({
1929+
registry,
1930+
config,
1931+
record: createPluginRecord({
1932+
id: "host-hook-fixture",
1933+
name: "Host Hook Fixture",
1934+
}),
1935+
register(api) {
1936+
api.registerControlUiDescriptor({
1937+
id: "approval-panel",
1938+
surface: "session",
1939+
label: "Approval panel",
1940+
});
1941+
},
1942+
});
1943+
const descriptorEntry = registry.registry.controlUiDescriptors?.[0];
1944+
if (!descriptorEntry) {
1945+
throw new Error("expected control UI descriptor registration");
1946+
}
1947+
Object.assign(descriptorEntry.descriptor, { leakedRegistryField: true });
1948+
setActivePluginRegistry(registry.registry);
1949+
1950+
const calls: Array<[boolean, unknown, unknown]> = [];
1951+
void pluginHostHookHandlers["plugins.uiDescriptors"]({
1952+
params: {},
1953+
respond: (ok: boolean, payload: unknown, error: unknown) => {
1954+
calls.push([ok, payload, error]);
1955+
},
1956+
} as never);
1957+
1958+
expect(calls).toHaveLength(1);
1959+
const [ok, payload, error] = calls[0] ?? [];
1960+
expect(ok).toBe(true);
1961+
expect(error).toBeUndefined();
1962+
expect(validatePluginsUiDescriptorsResult(payload)).toBe(true);
1963+
expect(payload).toEqual({
1964+
ok: true,
1965+
descriptors: [
1966+
{
1967+
id: "approval-panel",
1968+
pluginId: "host-hook-fixture",
1969+
pluginName: "Host Hook Fixture",
1970+
surface: "session",
1971+
label: "Approval panel",
1972+
},
1973+
],
1974+
});
18951975
});
18961976

18971977
it("enforces command requiredScopes for gateway clients and command owners", async () => {

0 commit comments

Comments
 (0)