refactor: share boundary open and gateway test helpers

This commit is contained in:
Peter Steinberger
2026-03-23 00:36:30 +00:00
parent b21bcf6eb6
commit 100d9a7a23
11 changed files with 140 additions and 91 deletions

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { isRecord } from "../utils.js";
import { DEFAULT_PLUGIN_ENTRY_CANDIDATES, PLUGIN_MANIFEST_FILENAME } from "./manifest.js";
import type { PluginBundleFormat } from "./types.js";
@@ -102,17 +102,19 @@ function loadBundleManifestFile(params: {
rejectHardlinks: params.rejectHardlinks,
});
if (!opened.ok) {
if (opened.reason === "path") {
if (params.allowMissing) {
return { ok: true, raw: {}, manifestPath };
}
return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath };
}
return {
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${opened.reason})`,
manifestPath,
};
return matchBoundaryFileOpenFailure(opened, {
path: () => {
if (params.allowMissing) {
return { ok: true, raw: {}, manifestPath };
}
return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath };
},
fallback: (failure) => ({
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
manifestPath,
}),
});
}
try {
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { applyMergePatch } from "../config/merge-patch.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { isRecord } from "../utils.js";
import {
CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH,
@@ -57,10 +57,18 @@ function readPluginJsonObject(params: {
rejectHardlinks: true,
});
if (!opened.ok) {
if (opened.reason === "path" && params.allowMissing) {
return { ok: true, raw: {} };
}
return { ok: false, error: `unable to read ${params.relativePath}: ${opened.reason}` };
return matchBoundaryFileOpenFailure(opened, {
path: () => {
if (params.allowMissing) {
return { ok: true, raw: {} };
}
return { ok: false, error: `unable to read ${params.relativePath}: path` };
},
fallback: (failure) => ({
ok: false,
error: `unable to read ${params.relativePath}: ${failure.reason}`,
}),
});
}
try {
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;

View File

@@ -7,34 +7,12 @@ import type {
SessionBindingAdapter,
SessionBindingRecord,
} from "../infra/outbound/session-binding-service.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import type { PluginRegistry } from "./registry.js";
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-binding-"));
const approvalsPath = path.join(tempRoot, "plugin-binding-approvals.json");
function createEmptyPluginRegistry(): PluginRegistry {
return {
plugins: [],
tools: [],
hooks: [],
typedHooks: [],
channels: [],
channelSetups: [],
providers: [],
speechProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
webSearchProviders: [],
gatewayHandlers: {},
httpRoutes: [],
cliRegistrars: [],
services: [],
commands: [],
conversationBindingResolvedHandlers: [],
diagnostics: [],
};
}
const sessionBindingState = vi.hoisted(() => {
const records = new Map<string, SessionBindingRecord>();
let nextId = 1;
@@ -105,9 +83,13 @@ const sessionBindingState = vi.hoisted(() => {
};
});
const pluginRuntimeState = vi.hoisted(() => ({
registry: createEmptyPluginRegistry(),
}));
const pluginRuntimeState = vi.hoisted(
() =>
({
// The runtime mock is initialized before imports; beforeEach installs the real shared stub.
registry: null as unknown as PluginRegistry,
}) satisfies { registry: PluginRegistry },
);
vi.mock("../infra/home-dir.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/home-dir.js")>();

View File

@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { resolveUserPath } from "../utils.js";
import { detectBundleManifestFormat, loadBundleManifest } from "./bundle-manifest.js";
import {
@@ -476,25 +476,25 @@ function resolvePackageEntrySource(params: {
rejectHardlinks: params.rejectHardlinks ?? true,
});
if (!opened.ok) {
if (opened.reason === "path") {
// File missing (ENOENT) — skip, not a security violation.
return null;
}
if (opened.reason === "io") {
// Filesystem error (EACCES, EMFILE, etc.) — warn but don't abort.
params.diagnostics.push({
level: "warn",
message: `extension entry unreadable (I/O error): ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
}
params.diagnostics.push({
level: "error",
message: `extension entry escapes package directory: ${params.entryPath}`,
source: params.sourceLabel,
return matchBoundaryFileOpenFailure(opened, {
path: () => null,
io: () => {
params.diagnostics.push({
level: "warn",
message: `extension entry unreadable (I/O error): ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
},
fallback: () => {
params.diagnostics.push({
level: "error",
message: `extension entry escapes package directory: ${params.entryPath}`,
source: params.sourceLabel,
});
return null;
},
});
return null;
}
const safeSource = opened.path;
fs.closeSync(opened.fd);

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { MANIFEST_KEY } from "../compat/legacy-names.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { isRecord } from "../utils.js";
import type { PluginConfigUiHint, PluginKind } from "./types.js";
@@ -159,14 +159,18 @@ export function loadPluginManifest(
rejectHardlinks,
});
if (!opened.ok) {
if (opened.reason === "path") {
return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath };
}
return {
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${opened.reason})`,
manifestPath,
};
return matchBoundaryFileOpenFailure(opened, {
path: () => ({
ok: false,
error: `plugin manifest not found: ${manifestPath}`,
manifestPath,
}),
fallback: (failure) => ({
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
manifestPath,
}),
});
}
let raw: unknown;
try {