fix(hooks): allow dot-prefixed handler paths

This commit is contained in:
Vincent Koc
2026-05-14 14:22:34 +08:00
parent e064cc98f0
commit 31de033590
3 changed files with 33 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
- Telegram: allow trusted local Bot API media files whose filenames start with dots instead of falling back to remote download.
- Agents/Codex app-server: remap injected context files under dot-dot-prefixed workspace directories when a run switches to an effective sandbox workspace.
- Control UI/i18n: use the installed workspace pi runtime for locale refreshes, update the fallback package pin, and skip scheduled refreshes with invalid provider credentials instead of failing main.
- Hooks: load workspace-relative legacy hook modules from dot-dot-prefixed directories without treating the filename prefix as parent traversal.
- Plugins: preserve installed package metadata and persisted registry freshness checks for plugin package paths under dot-dot-prefixed directories.
- Agents: allow dot-dot-prefixed filenames such as `..note.txt` through sandbox FS bridge, remote sandbox reads, and apply_patch summaries without mistaking the name for parent traversal.
- CLI/migrate: hide per-item source/plugin hints on non-conflicting Codex skill and plugin selection prompts, keeping the hint text reserved for rows that actually need attention. Thanks @sjf.

View File

@@ -261,6 +261,28 @@ describe("loader", () => {
expect(keys).toContain("command:stop");
});
it("loads legacy handler modules from dot-prefixed workspace paths", async () => {
await fs.mkdir(path.join(tmpDir, "..hooks"), { recursive: true });
await writeHandlerModule(
path.join("..hooks", "legacy-handler.js"),
'export default async function(event) { event.messages.push("dot-prefixed-hook"); }\n',
);
const cfg = createEnabledHooksConfig([
{
event: "command:new",
module: path.join("..hooks", "legacy-handler.js"),
},
]);
const count = await loadInternalHooks(cfg, tmpDir);
expect(count).toBe(1);
const event = createInternalHookEvent("command", "new", "test-session");
await triggerInternalHook(event);
expect(event.messages).toEqual(["dot-prefixed-hook"]);
});
it("preserves plugin-registered hooks when workspace hooks reload", async () => {
const pluginHandler = vi.fn();
registerInternalHook("gateway:startup", pluginHandler);

View File

@@ -34,6 +34,15 @@ function safeLogValue(value: string): string {
return sanitizeForLog(value);
}
function isNonEmptyRelativePathInsideRoot(relativePath: string): boolean {
return (
relativePath !== "" &&
relativePath !== ".." &&
!relativePath.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relativePath)
);
}
function maybeWarnTrustedHookSource(source: string): void {
if (source === "openclaw-workspace") {
log.warn(
@@ -211,7 +220,7 @@ export async function loadInternalHooks(
continue;
}
const rel = path.relative(baseDirReal, modulePathSafe);
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
if (!isNonEmptyRelativePathInsideRoot(rel)) {
log.error(`Handler module path must stay within workspaceDir: ${safeLogValue(rawModule)}`);
continue;
}