fix: bound skills watcher traversal

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
sallyom
2026-05-07 18:12:08 -04:00
committed by Sally O'Malley
parent 5df08201ff
commit 244c2b5b23
3 changed files with 33 additions and 1 deletions
+1
View File
@@ -160,6 +160,7 @@ Docs: https://docs.openclaw.ai
- Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058.
- Agents/subagents: retry parent wake announces when the announce-summary model run fails with fallback cooldown exhaustion instead of dropping the wake on the first transient provider overload. Refs #78581.
- Providers/network: honor IPv4 CIDR and octet-wildcard `NO_PROXY` entries such as `100.64.0.0/10` and `100.64.*` before enabling trusted env-proxy mode for model-provider requests. Fixes #79030.
- Skills: cap skills watcher directory traversal at the same depth used by skill discovery so large non-skill trees under configured skill roots do not exhaust file descriptors on startup. Fixes #75501. Thanks @wzq-xzwj.
- Docs/Docker: document a local Compose override for Docker Desktop DNS failures in the shared-network `openclaw-cli` sidecar, keeping the default compose setup hardened while unblocking `openclaw plugins install` when users opt in. Fixes #79018. Thanks @Jason-Vaughan.
- Installer: when npm installs `openclaw` outside the parent shell PATH, print follow-up commands with the resolved binary path instead of telling users to run `openclaw` from a shell that will report `command not found`. Fixes #72382. Thanks @jbob762.
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
+30 -1
View File
@@ -60,12 +60,13 @@ describe("ensureSkillsWatcher", () => {
expect(watchMock).toHaveBeenCalledTimes(1);
const firstCall = (
watchMock.mock.calls as unknown as Array<[string[], { ignored?: unknown }]>
watchMock.mock.calls as unknown as Array<[string[], { depth?: number; ignored?: unknown }]>
)[0];
const targets = firstCall?.[0] ?? [];
const opts = firstCall?.[1] ?? {};
expect(opts.ignored).toBe(refreshModule.shouldIgnoreSkillsWatchPath);
expect(opts.depth).toBe(2);
const posix = (p: string) => p.replaceAll("\\", "/");
expect(targets).toEqual(
expect.arrayContaining([
@@ -100,6 +101,34 @@ describe("ensureSkillsWatcher", () => {
expect(ignored("/tmp/workspace/skills/my-skill/SKILL.md", {})).toBe(false);
});
it("keeps grouped skill folders within the watcher traversal depth", async () => {
vi.useFakeTimers();
const seen: SkillsChangeEvent[] = [];
refreshModule.registerSkillsChangeListener((change) => {
seen.push(change);
});
refreshModule.ensureSkillsWatcher({
workspaceDir: "/tmp/workspace",
config: { skills: { load: { watchDebounceMs: 10 } } },
});
const firstCall = (
watchMock.mock.calls as unknown as Array<[string[], { depth?: number; ignored?: unknown }]>
)[0];
expect(firstCall?.[1]?.depth).toBe(2);
createdWatchers[0]?.emit("change", "/tmp/workspace/skills/group/demo/SKILL.md");
await vi.advanceTimersByTimeAsync(10);
expect(seen).toEqual([
{
workspaceDir: "/tmp/workspace",
reason: "watch",
changedPath: "/tmp/workspace/skills/group/demo/SKILL.md",
},
]);
});
it.each(["add", "change", "unlink", "unlinkDir"] as const)(
"refreshes skills snapshots on %s",
async (event) => {
+2
View File
@@ -141,6 +141,8 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Ope
const watcher = chokidar.watch(watchTargets, {
ignoreInitial: true,
// Skill discovery reads root skills, direct child skills, and one grouped skill level.
depth: 2,
awaitWriteFinish: {
stabilityThreshold: debounceMs,
pollInterval: 100,