From d98a4830906751c234d722f5b9d5201a91e68b8c Mon Sep 17 00:00:00 2001 From: Intern Dev Date: Mon, 27 Apr 2026 11:30:17 -0400 Subject: [PATCH] fix(policy): ignore dotfile presets from directories Directory-based custom preset loading should mirror normal directory scans by ignoring hidden dotfiles such as .draft.yaml. This keeps --from-dir focused on intentionally selected visible preset files. Constraint: Preserve sorted application order and first-failure abort semantics for visible YAML files. Rejected: Filter dotfiles inside loadPresetFromFile | single-file loading should still honor an explicit user path. Confidence: high Scope-risk: narrow Tested: npm run build:cli Tested: npm run typecheck:cli Tested: npm test -- test/policies.test.ts -t='--from-dir skips hidden dotfile yaml presets' Tested: git diff --check --- src/nemoclaw.ts | 11 +++++++---- test/policies.test.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index 3f13740416..47a1aeb076 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -1817,9 +1817,9 @@ function buildSandboxLogsArgs(sandboxName: string, follow: boolean): string[] { * for a single custom preset YAML, and `--from-dir ` for every * `.yaml`/`.yml` file in a directory. `--dry-run` previews without applying, * `--yes`/`-y`/`--force` (or `NEMOCLAW_NON_INTERACTIVE=1`) skips the - * confirmation prompt. `--from-dir` applies files in lexicographic order - * and aborts at the first failure (already-applied presets are not rolled - * back). + * confirmation prompt. `--from-dir` applies non-hidden files in lexicographic + * order and aborts at the first failure (already-applied presets are not + * rolled back). */ async function sandboxPolicyAdd(sandboxName: string, args: string[] = []): Promise { const dryRun = args.includes("--dry-run"); @@ -1861,7 +1861,10 @@ async function sandboxPolicyAdd(sandboxName: string, args: string[] = []): Promi } const files = fs .readdirSync(absDir, { withFileTypes: true }) - .filter((ent: { name: string; isFile(): boolean }) => ent.isFile() && /\.ya?ml$/i.test(ent.name)) + .filter( + (ent: { name: string; isFile(): boolean }) => + ent.isFile() && !ent.name.startsWith(".") && /\.ya?ml$/i.test(ent.name), + ) .map((ent: { name: string }) => path.join(absDir, ent.name)) .sort(); if (files.length === 0) { diff --git a/test/policies.test.ts b/test/policies.test.ts index f2cfb8afe3..da5d4a7125 100644 --- a/test/policies.test.ts +++ b/test/policies.test.ts @@ -1496,5 +1496,23 @@ setImmediate(() => { expect(loads.length).toBe(1); expect(loads[0]).toMatch(/real\.yaml$/); }); + + it("--from-dir skips hidden dotfile yaml presets", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-from-dir-dotfile-")); + fs.writeFileSync( + path.join(dir, ".bad.yaml"), + "preset:\n name: bad\nnetwork_policies: {}\n", + ); + fs.writeFileSync( + path.join(dir, "real.yaml"), + "preset:\n name: real\nnetwork_policies: {}\n", + ); + const result = runPolicyAddExternal(["--from-dir", dir, "--yes"]); + expect(result.status).toBe(0); + const calls = JSON.parse(result.stdout.split("__CALLS__")[1].trim()) as PolicyCall[]; + const loads = calls.filter((c) => c.type === "load").map((c) => c.path); + expect(loads.length).toBe(1); + expect(loads[0]).toMatch(/real\.yaml$/); + }); }); });