Bound plugin bundle command file reads with size cap (#110594)

* Bound bundle command file reads with size cap

* fix: use buffer.toString for readRegularFileSync result

* fix: log oversized bundle command file diagnostic instead of silent skip

The catch block now captures the error and emits a console.warn with the file path and error detail, so upgrades do not silently remove oversized installed commands.

* test: verify oversized bundle command file is skipped and siblings continue

PR #110594: Add focused regression coverage:
- Test: an oversized bundle command markdown file (>1 MB) is skipped via catch + continue
- Test: normal sibling command files still load correctly
- Verifies console.warn diagnostic is emitted for the oversized file

* refactor(plugins): log rejected bundle commands

Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>

* style(plugins): format bundle warning

Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
cxbAsDev
2026-07-19 06:03:00 +08:00
committed by GitHub
parent f3f6eb2321
commit 7bf263b8b6
2 changed files with 59 additions and 2 deletions
+48
View File
@@ -8,6 +8,11 @@ import type { PluginManifestRecord } from "./manifest-registry.js";
const mocks = vi.hoisted(() => ({
plugins: [] as PluginManifestRecord[],
warn: vi.fn(),
}));
vi.mock("../logging/subsystem.js", () => ({
createSubsystemLogger: () => ({ warn: mocks.warn }),
}));
vi.mock("./manifest-registry.js", () => ({
@@ -36,6 +41,7 @@ const tempDirs: string[] = [];
afterEach(async () => {
mocks.plugins = [];
mocks.warn.mockReset();
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
@@ -224,4 +230,46 @@ describe("loadEnabledClaudeBundleCommands", () => {
},
);
});
it("warns and skips oversized bundle commands without dropping siblings", async () => {
const homeDir = await createTempDir("openclaw-bundle-commands-oversized-");
const workspaceDir = await createTempDir("openclaw-bundle-commands-oversized-ws-");
await writeClaudeBundleCommandFixture({
homeDir,
pluginId: "oversized-test",
commands: [
{
relativePath: "commands/normal.md",
contents: [
"---",
"description: Normal command that should be loaded",
"---",
"This is a normal command.",
],
},
],
});
const pluginRoot = resolveBundlePluginRoot(homeDir, "oversized-test");
const oversizedFilePath = path.join(pluginRoot, "commands", "oversized.md");
await fs.mkdir(path.dirname(oversizedFilePath), { recursive: true });
const oversizedContent = Buffer.alloc(1 * 1024 * 1024 + 1, "x");
await fs.writeFile(oversizedFilePath, oversizedContent);
const commands = loadEnabledClaudeBundleCommands({
workspaceDir,
cfg: {
plugins: {
entries: { "oversized-test": { enabled: true } },
},
},
});
expect(commands.map((entry) => entry.rawName)).toEqual(["normal"]);
expect(mocks.warn).toHaveBeenCalledOnce();
const warning = String(mocks.warn.mock.calls[0]?.[0]);
expect(warning).toContain(oversizedFilePath);
expect(warning).toContain("1048576");
});
});
+11 -2
View File
@@ -10,7 +10,10 @@ import {
stripFrontmatterBlock,
} from "../../packages/markdown-core/src/frontmatter.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { readRootJsonObjectSync } from "../infra/json-files.js";
import { readRegularFileSync } from "../infra/regular-file.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { isPathInsideWithRealpath } from "../security/scan-paths.js";
import { parseFrontmatterBool } from "../shared/frontmatter.js";
import {
@@ -33,6 +36,9 @@ type ClaudeBundleCommandSpec = {
sourceFilePath: string;
};
const BUNDLE_COMMAND_MAX_BYTES = 1 * 1024 * 1024;
const log = createSubsystemLogger("plugins/bundle-commands");
function readClaudeBundleManifest(rootDir: string): Record<string, unknown> {
const result = readRootJsonObjectSync({
rootDir,
@@ -103,8 +109,11 @@ function loadBundleCommandsFromRoot(params: {
for (const filePath of listMarkdownFilesRecursive(params.commandRoot)) {
let raw: string;
try {
raw = fs.readFileSync(filePath, "utf-8");
} catch {
raw = readRegularFileSync({ filePath, maxBytes: BUNDLE_COMMAND_MAX_BYTES }).buffer.toString(
"utf-8",
);
} catch (error) {
log.warn(`skipping unreadable bundle command file ${filePath}: ${formatErrorMessage(error)}`);
continue;
}
const frontmatter = parseFrontmatterBlock(raw);