fix(#2665): stop watching shared ground, and derive the artifact prefix too

The previous commit widened the guard's watch set and claimed the enumeration
was complete. Re-running the pre-push adversarial gate on that commit -- which I
should have done before pushing it, and did not -- refuted the claim on four
counts. All four were real.

1. FALSE POSITIVES, which is the worse polarity. `hooks/lib`,
   `hooks/package.json`, `scripts/lib` and `scripts/changeset` were watched
   WHOLESALE. The installer preserves foreign files in every one of them -- it
   removes the CommonJS marker only on an exact content match, because "a
   user-authored package.json is never deleted" -- so a user editing their own
   helper mid-suite tripped the guard. A driven probe produced four violations
   from touching only user-owned files. Watching shared ground is exactly what
   the module's SCOPE note refuses: a guard that cries wolf gets switched off,
   and then catches nothing at all. Now only exact GSD filenames inside those
   dirs are watched, and a test asserts foreign edits stay silent.

2. THE PREFIX WAS HARDCODED, which is this PR's own defect one level down. Each
   artifactLayout declares its OWN prefix, and kimi's `kimi-agents` layout
   declares `gsd` with no hyphen, writing `agents/gsd.yaml` and `agents/gsd.md`.
   A fixed `gsd-` scan is structurally blind to both, as it is to pi's
   `extensions/gsd.js`. The prefix is now derived per parent, as a SET -- the
   same destSubpath carries different prefixes across runtimes (`agents` appears
   with both `gsd` and `gsd-`). `extensions` joins the non-registry parents; pi
   declares no artifactLayout at all, so no registry walk could find it.

3. THE ENTRY BOUND FAILED OPEN on a non-finite limit: `Math.max(0, NaN)` is NaN,
   and every budget comparison against NaN is false, so the walk was unbounded --
   the single thing the constant exists to prevent. Clamped with Number.isFinite.
   The walk also kept invoking itself for every remaining sibling after the
   budget was gone; it now returns.

4. THE RESIDUAL LIST WAS WRONG AGAIN. `agents/subagents/**` (kimi stages under an
   unprefixed intermediate dir), the loose capability generators, and the
   `extensions`/`plugins` CommonJS markers are all unwatched and were unnamed.
   They are named now, and the four shared dirs are recorded as DELIBERATELY not
   watched -- a different thing from missed.

Each fix is negative-controlled and each control fires. The NaN control did not
fire on its first form: the test asserted `truncated: false`, which the broken
code also produces on a small tree, so it discriminated nothing. Repaired with a
NaN perTarget against a small finite ceiling, where the two behaviours differ.
This commit is contained in:
0xdhx
2026-08-06 17:51:52 -05:00
parent 766480967e
commit fae0c6ae1a
2 changed files with 154 additions and 49 deletions

View File

@@ -246,32 +246,41 @@ describe('#2665: live-config hermeticity guard', () => {
// NB: `workflows` is declared only in LOCAL scope (windsurf), so it is
// deliberately NOT a global config-root parent -- the derivation walks
// artifactLayout.global only.
for (const p of ['agents', 'commands', 'command', 'skills', 'hooks', 'plugins', 'scripts']) {
assert.ok(parents.includes(p), `expected derived parent ${p} in ${JSON.stringify(parents)}`);
for (const p of ['agents', 'commands', 'command', 'skills', 'hooks', 'plugins', 'scripts', 'extensions']) {
assert.ok(p in parents, `expected derived parent ${p} in ${JSON.stringify(Object.keys(parents))}`);
}
// kimi's kimi-agents layout declares prefix `gsd` (no hyphen); a fixed `gsd-`
// scan cannot see agents/gsd.yaml, which is the defect this derivation closes.
assert.ok(
parents.agents.includes('gsd'),
`agents must carry kimi's bare 'gsd' prefix; got ${JSON.stringify(parents.agents)}`,
);
assert.ok(owned.includes('skills/gsd'), `expected hermes skills/gsd in ${JSON.stringify(owned)}`);
for (const o of ['hooks/lib', 'scripts/lib']) {
// Exact GSD filenames only — never the shared directories that contain them.
for (const o of ['scripts/fix-slash-commands.cjs', 'hooks/managed-hooks-registry.cjs']) {
assert.ok(owned.includes(o), `expected owned nested ${o} in ${JSON.stringify(owned)}`);
}
for (const shared of ['hooks/lib', 'hooks/package.json', 'scripts/lib', 'scripts/changeset']) {
assert.ok(!owned.includes(shared), `${shared} is shared ground and must NOT be watched wholesale`);
}
});
test('detects leaks the gsd- prefix rule structurally cannot see', () => {
test('detects leaks a single hardcoded gsd- prefix cannot see', () => {
const root = tmpRoot();
try {
for (const d of ['skills/gsd', 'hooks/lib', 'plugins', 'command', 'scripts/lib']) {
for (const d of ['skills/gsd', 'agents', 'plugins', 'command', 'extensions']) {
fs.mkdirSync(path.join(root, ...d.split('/')), { recursive: true });
}
const before = snapshotLiveConfig([root]);
// Pre-existing dirs register as MODIFIED, so bump mtimes explicitly rather
// than racing a coarse filesystem clock -- same reason the `modified` test
// above uses utimesSync.
const future = new Date(Date.now() + 10000);
// kimi declares prefix `gsd` (no hyphen) and writes agents/gsd.yaml;
// pi writes extensions/gsd.js. A fixed `gsd-` scan sees neither.
const leaks = [
['skills', 'gsd', 'executor.md'],
['hooks', 'lib', 'git-cmd.js'],
['agents', 'gsd.yaml'],
['extensions', 'gsd.js'],
['plugins', 'gsd-core.js'],
['command', 'gsd-plan.md'],
['scripts', 'lib', 'io.cjs'],
];
for (const seg of leaks) {
const f = path.join(root, ...seg);
@@ -281,7 +290,7 @@ describe('#2665: live-config hermeticity guard', () => {
}
const hit = diffLiveConfig(before, snapshotLiveConfig([root])).map((v) => v.path);
for (const expected of ['skills/gsd', 'hooks/lib', 'plugins/gsd-core.js', 'command/gsd-plan.md', 'scripts/lib']) {
for (const expected of ['skills/gsd', 'agents/gsd.yaml', 'extensions/gsd.js', 'plugins/gsd-core.js', 'command/gsd-plan.md']) {
const abs = path.join(root, ...expected.split('/'));
assert.ok(hit.includes(abs), `${expected} leaked undetected; got ${JSON.stringify(hit)}`);
}
@@ -290,6 +299,42 @@ describe('#2665: live-config hermeticity guard', () => {
}
});
test('a user editing their OWN files in a shared dir is not a violation', () => {
// The false-positive case a prior commit shipped: hooks/lib, hooks/package.json,
// scripts/lib and scripts/changeset were watched WHOLESALE, so touching a
// user-authored helper in any of them tripped the guard. The installer itself
// preserves foreign files in all four, so they are not GSD's to watch.
const root = tmpRoot();
try {
for (const d of ['hooks/lib', 'scripts/lib', 'scripts/changeset']) {
fs.mkdirSync(path.join(root, ...d.split('/')), { recursive: true });
}
const foreign = [
['hooks', 'package.json'],
['hooks', 'lib', 'user-helper.js'],
['scripts', 'lib', 'user-helper.cjs'],
['scripts', 'changeset', 'user-tool.cjs'],
];
for (const seg of foreign) fs.writeFileSync(path.join(root, ...seg), 'mine');
const before = snapshotLiveConfig([root]);
const future = new Date(Date.now() + 10000);
for (const seg of foreign) {
const f = path.join(root, ...seg);
fs.writeFileSync(f, 'mine, edited');
fs.utimesSync(f, future, future);
fs.utimesSync(path.dirname(f), future, future);
}
assert.deepStrictEqual(
diffLiveConfig(before, snapshotLiveConfig([root])),
[],
'editing user-owned files in a shared dir must not trip the guard',
);
} finally {
cleanup(root);
}
});
test('extra watch targets cover the fallback root as well as the ambient one', () => {
// The MISSED finding from round 5's review: B3 closed this for the registry
// roots and left the identical hole in resolveExtraWatchTargets.
@@ -380,6 +425,26 @@ describe('#2665: live-config hermeticity guard', () => {
}
});
test('a non-finite injected limit falls back to the real bound, never fails open', () => {
// Math.max(0, NaN) is NaN, and every budget comparison against NaN is false,
// so the walk becomes unbounded — the one thing the bound exists to prevent.
// The discriminator has to be a case where the two behaviours DIFFER: pair a
// NaN perTarget with a small finite ceiling. Fixed, perTarget falls back to
// MAX_ENTRIES and the ceiling still bites (truncated). Broken, min(NaN, 3) is
// NaN and nothing truncates at all.
const [dir] = treeWithEntries(6);
try {
const snap = snapshotLiveConfig([], [dir], { perTarget: NaN, total: 3 });
assert.strictEqual(
snap[path.resolve(dir)].truncated,
true,
'a NaN perTarget must fall back to a real bound, not disable budgeting',
);
} finally {
cleanup(dir);
}
});
test('the GLOBAL ceiling still bounds the aggregate, and reports unverified', () => {
// The bound the single budget was really for is kept -- but when it engages,
// the curtailed target is reported rather than silently attested clean.