fix(#4132): verify durable runtime surface sources (#4182)

* fix(#4132): verify durable runtime surface sources

* chore(#4132): record PR number in changeset

* test(#4132): cover rejected commands source alias

* fix(#4132): reject aliased package fallback

* test(#4132): cover rejected agents source alias

* test(#4132): cover partially aliased marker provider

* fix(#4132): reject partially aliased source providers

* test(#4132): cover routed source identity probes

* fix(#4132): route installed source identity probes

* refactor(#4132): tighten installer source metadata

* test(#4132): cover corpus trust boundary attacks

* fix(#4132): close installed corpus trust gaps

* refactor(#4132): keep installer authority private

* fix(#4132): preserve private installer fallback

* test(#4132): preserve fixture source authority

* fix(#4132): reject overlapping source fallback

* fix(#4132): avoid redundant installed corpus reads

* refactor(#4132): simplify provider resolution

* test(#4132): sync install tree fixtures after rebase

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Zy Deng
2026-09-05 21:32:52 +02:00
committed by GitHub
parent 1017898cb9
commit 4c60879b5d
49 changed files with 3956 additions and 307 deletions

View File

@@ -29,7 +29,7 @@ const os = require('node:os');
const espree = require('espree');
const { splitLines, joinLines } = require('../gsd-core/bin/lib/text-lines.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup, writePackageSourceMarkerFixture } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const {
@@ -172,6 +172,299 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
});
});
describe('installRuntimeArtifacts — durable Runtime Surface corpus (#4132)', () => {
test('#4132: provisioning refuses a gsd-core ancestor symlink that escapes configDir', (t) => {
const root = createTempDir('gsd-corpus-parent-symlink-');
const configDir = path.join(root, 'config');
const outside = path.join(root, 'outside');
fs.mkdirSync(configDir);
fs.mkdirSync(outside);
t.after(() => cleanup(root));
try {
fs.symlinkSync(outside, path.join(configDir, 'gsd-core'), process.platform === 'win32' ? 'junction' : 'dir');
} catch (error) {
t.skip(`directory symlink creation unavailable: ${error.code || error.message}`);
return;
}
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/symlink|refusing/i,
);
assert.equal(fs.existsSync(path.join(outside, 'commands')), false, 'commands must not escape configDir');
assert.equal(fs.existsSync(path.join(outside, 'agents')), false, 'agents must not escape configDir');
});
test('#4132: provisioning does not follow a compatibility-marker symlink outside configDir', (t) => {
const root = createTempDir('gsd-corpus-marker-symlink-');
const configDir = path.join(root, '.claude');
const outsideMarker = path.join(root, 'outside-marker');
const outsideCheckout = path.join(root, 'outside-checkout');
fs.mkdirSync(configDir);
fs.cpSync(path.join(__dirname, '..', 'commands'), path.join(outsideCheckout, 'commands'), { recursive: true });
fs.cpSync(path.join(__dirname, '..', 'agents'), path.join(outsideCheckout, 'agents'), { recursive: true });
fs.appendFileSync(path.join(outsideCheckout, 'commands', 'gsd', 'help.md'), '\nMARKER_SYMLINK_SOURCE\n');
const outsideMarkerBytes = path.join(outsideCheckout, 'commands', 'gsd') + '\n';
fs.writeFileSync(outsideMarker, outsideMarkerBytes);
t.after(() => cleanup(root));
try {
fs.symlinkSync(outsideMarker, path.join(configDir, '.gsd-source'), 'file');
} catch (error) {
t.skip(`file symlink creation unavailable: ${error.code || error.message}`);
return;
}
runMinimalInstall({ runtime: 'claude', scope: 'global', root });
assert.equal(
fs.readFileSync(outsideMarker, 'utf8'),
outsideMarkerBytes,
'the compatibility marker write must remain confined to configDir',
);
assert.doesNotMatch(
fs.readFileSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md'), 'utf8'),
/MARKER_SYMLINK_SOURCE/,
'the installer must not read a source provider through a symlinked marker file',
);
});
test('global Codex owns the canonical commands and agents corpus', (t) => {
const configDir = createTempDir('gsd-codex-surface-corpus-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.deepStrictEqual(
snapshot2362Dir(path.join(configDir, 'gsd-core', 'commands', 'gsd')),
snapshot2362Dir(path.join(__dirname, '..', 'commands', 'gsd')),
'installed commands corpus must match the package source tree',
);
assert.deepStrictEqual(
snapshot2362Dir(path.join(configDir, 'gsd-core', 'agents')),
snapshot2362Dir(path.join(__dirname, '..', 'agents')),
'installed agents corpus must match the package source tree',
);
});
test('agents-only and empty layouts derive corpus needs from layout kinds', (t) => {
const windsurfDir = createTempDir('gsd-windsurf-surface-corpus-');
const piDir = createTempDir('gsd-pi-surface-corpus-');
const vscodeDir = createTempDir('gsd-vscode-surface-corpus-');
t.after(() => { cleanup(windsurfDir); cleanup(piDir); cleanup(vscodeDir); });
installRuntimeArtifacts('windsurf', windsurfDir, 'global', RESOLVED_CORE);
assert.ok(fs.existsSync(path.join(windsurfDir, 'gsd-core', 'agents')));
assert.ok(!fs.existsSync(path.join(windsurfDir, 'gsd-core', 'commands', 'gsd')));
installRuntimeArtifacts('pi', piDir, 'global', RESOLVED_CORE);
installRuntimeArtifacts('vscode', vscodeDir, 'global', RESOLVED_CORE);
assert.ok(!fs.existsSync(path.join(piDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(piDir, 'gsd-core', 'agents')));
assert.ok(!fs.existsSync(path.join(vscodeDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(vscodeDir, 'gsd-core', 'agents')));
});
test('local Claude and Codex installs do not gain the global corpus', (t) => {
const claudeDir = createTempDir('gsd-claude-local-no-corpus-');
const codexDir = createTempDir('gsd-codex-local-no-corpus-');
t.after(() => { cleanup(claudeDir); cleanup(codexDir); });
installRuntimeArtifacts('claude', claudeDir, 'local', RESOLVED_CORE);
installRuntimeArtifacts('codex', codexDir, 'local', RESOLVED_CORE);
for (const configDir of [claudeDir, codexDir]) {
assert.ok(!fs.existsSync(path.join(configDir, 'gsd-core', 'commands')));
assert.ok(!fs.existsSync(path.join(configDir, 'gsd-core', 'agents')));
}
});
test('refresh restores canonical bytes and removes only manifest-proven stale corpus files', (t) => {
const configDir = createTempDir('gsd-corpus-refresh-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const canonicalName = fs.readdirSync(commandsDir).find((name) => name.endsWith('.md'));
assert.ok(canonicalName);
fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n');
fs.writeFileSync(path.join(commandsDir, 'stale-owned.md'), '# stale\n');
fs.writeFileSync(path.join(commandsDir, 'unknown-neighbour.md'), '# preserve\n');
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
files: { 'gsd-core/commands/gsd/stale-owned.md': '0'.repeat(64) },
}));
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.strictEqual(
fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'),
fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', canonicalName), 'utf8'),
);
assert.ok(!fs.existsSync(path.join(commandsDir, 'stale-owned.md')));
assert.strictEqual(fs.readFileSync(path.join(commandsDir, 'unknown-neighbour.md'), 'utf8'), '# preserve\n');
});
test('a partial corpus copy failure is rejected until a later install repairs it', (t) => {
const configDir = createTempDir('gsd-corpus-copy-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const agentsDestination = path.join(configDir, 'gsd-core', 'agents');
const originalCpSync = fs.cpSync;
t.mock.method(fs, 'cpSync', (source, destination, options) => {
if (destination === agentsDestination) {
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, 'gsd-planner.md'), '# partial agent\n');
throw new Error('injected corpus copy failure');
}
return originalCpSync(source, destination, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected corpus copy failure/,
);
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
t.mock.restoreAll();
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.deepStrictEqual(
snapshot2362Dir(agentsDestination),
snapshot2362Dir(path.join(__dirname, '..', 'agents')),
'a later install must heal the partial agents corpus',
);
});
test('an unreadable prior manifest preserves unproven neighbours while refreshing canonical bytes', (t) => {
const configDir = createTempDir('gsd-corpus-manifest-read-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
fs.mkdirSync(commandsDir, { recursive: true });
const canonicalName = fs.readdirSync(path.join(__dirname, '..', 'commands', 'gsd'))
.find((name) => name.endsWith('.md'));
assert.ok(canonicalName);
fs.writeFileSync(path.join(commandsDir, canonicalName), '# corrupted\n');
fs.writeFileSync(path.join(commandsDir, 'unproven-neighbour.md'), '# preserve\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify({ files: {
'gsd-core/commands/gsd/unproven-neighbour.md': '0'.repeat(64),
} }));
const originalReadFileSync = fs.readFileSync;
t.mock.method(fs, 'readFileSync', (filePath, ...args) => {
if (filePath === manifestPath) throw new Error('injected manifest read failure');
return originalReadFileSync(filePath, ...args);
});
installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE);
assert.equal(
fs.readFileSync(path.join(commandsDir, canonicalName), 'utf8'),
fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', canonicalName), 'utf8'),
);
assert.equal(
fs.readFileSync(path.join(commandsDir, 'unproven-neighbour.md'), 'utf8'),
'# preserve\n',
'an unreadable manifest provides no ownership proof for deletion',
);
});
test('a stale corpus removal failure leaves a resolver-rejected corpus', (t) => {
const configDir = createTempDir('gsd-corpus-remove-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const agentsDir = path.join(configDir, 'gsd-core', 'agents');
fs.mkdirSync(commandsDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
const stalePath = path.join(commandsDir, 'removed-by-4132-test.md');
fs.writeFileSync(stalePath, '# stale\n');
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), '# stale agent\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestBytes = JSON.stringify({ files: {
'gsd-core/commands/gsd/removed-by-4132-test.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} });
fs.writeFileSync(manifestPath, manifestBytes);
const originalRmSync = fs.rmSync;
t.mock.method(fs, 'rmSync', (target, options) => {
if (target === stalePath) throw new Error('injected stale corpus removal failure');
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- delegate non-target production writes; fixture cleanup still uses cleanup().
return originalRmSync(target, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected stale corpus removal failure/,
);
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest');
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
});
test('an empty-parent prune failure leaves a resolver-rejected corpus', (t) => {
const configDir = createTempDir('gsd-corpus-prune-failure-');
t.after(() => cleanup(configDir));
sandboxHome(t, configDir);
const commandsDir = path.join(configDir, 'gsd-core', 'commands', 'gsd');
const agentsDir = path.join(configDir, 'gsd-core', 'agents');
const staleParent = path.join(commandsDir, 'retired');
const stalePath = path.join(staleParent, 'removed-by-4132-test.md');
fs.mkdirSync(staleParent, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(stalePath, '# stale\n');
fs.writeFileSync(path.join(agentsDir, 'gsd-planner.md'), '# stale agent\n');
const manifestPath = path.join(configDir, 'gsd-file-manifest.json');
const manifestBytes = JSON.stringify({ files: {
'gsd-core/commands/gsd/retired/removed-by-4132-test.md': '0'.repeat(64),
'gsd-core/agents/gsd-planner.md': '0'.repeat(64),
} });
fs.writeFileSync(manifestPath, manifestBytes);
const originalRmdirSync = fs.rmdirSync;
t.mock.method(fs, 'rmdirSync', (target, options) => {
if (target === staleParent) throw new Error('injected corpus parent prune failure');
return originalRmdirSync(target, options);
});
assert.throws(
() => installRuntimeArtifacts('codex', configDir, 'global', RESOLVED_CORE),
/injected corpus parent prune failure/,
);
assert.equal(fs.existsSync(stalePath), false, 'owned stale file was removed before parent pruning failed');
assert.equal(fs.existsSync(staleParent), true, 'failed empty parent removal must leave the directory in place');
assert.equal(fs.readFileSync(manifestPath, 'utf8'), manifestBytes, 'failed provisioning must not claim a new manifest');
const incompleteLayout = resolveRuntimeArtifactLayout('codex', configDir, 'global');
assert.throws(
() => incompleteLayout.kinds.find((kind) => kind.kind === 'skills').stage(RESOLVED_CORE),
/install or upgrade gsd-core/,
);
});
for (const runtime of ['codex', 'claude']) {
test(`full global ${runtime} manifest covers every installed corpus file with its hash`, (t) => {
const installed = runMinimalInstall({ runtime, scope: 'global' });
t.after(() => cleanup(installed.root));
const corpusRoots = [
['gsd-core/commands/gsd/', path.join(installed.configDir, 'gsd-core', 'commands', 'gsd')],
['gsd-core/agents/', path.join(installed.configDir, 'gsd-core', 'agents')],
];
for (const [prefix, root] of corpusRoots) {
for (const file of walk(root)) {
const key = prefix + path.relative(root, file).replace(/\\/g, '/');
const hash = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
assert.strictEqual(installed.manifest.files[key], hash, `${key} must be manifest-owned with the installed hash`);
}
}
});
}
});
const SKILLS_RUNTIMES_LAYOUT = [
'claude', 'cursor', 'codex', 'copilot', 'antigravity',
'augment', 'trae', 'qwen', 'kimi', 'codebuddy',
@@ -415,6 +708,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: writes gsd-help/SKILL.md with name + description`, (t) => {
const configDir = createTempDir(`gsd-ocs-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
const raw = stageRawCommands(runtime, configDir);
const count = installOpencodeFamilySkills(runtime, configDir, raw, `${configDir}/`);
@@ -431,6 +725,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: rewrites body paths to the actual install target (#784 path fix)`, (t) => {
const configDir = createTempDir(`gsd-ocp-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
// Simulate a custom/local install: pathPrefix points at configDir, NOT the
// runtime's default global config dir. Body refs must use pathPrefix.
@@ -461,6 +756,7 @@ describe('installOpencodeFamilySkills — emits skills/<name>/SKILL.md (#784)',
test(`${runtime}: preserves user-owned gsd-dev-preferences across reinstall (#784)`, (t) => {
const configDir = createTempDir(`gsd-ocd-${runtime}-`);
t.after(() => cleanup(configDir));
writePackageSourceMarkerFixture(configDir);
const userSkill = path.join(configDir, 'skills', 'gsd-dev-preferences');
fs.mkdirSync(userSkill, { recursive: true });
@@ -2656,6 +2952,7 @@ describe('fix-2644 — Cursor has one menu entry per GSD workflow', () => {
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'core',
files: { 'commands/gsd-help.md': crypto.createHash('sha256').update(content).digest('hex') },
}));
writePackageSourceMarkerFixture(configDir);
const layout = resolveRuntimeArtifactLayout('cursor', configDir, 'global');
applySurface(configDir, layout, MANIFEST);
@@ -5582,7 +5879,13 @@ const {
*/
function writeMinimalSourceTree(baseDir, stems) {
const srcDir = path.join(baseDir, 'src', 'commands', 'gsd');
const agentsDir = path.join(baseDir, 'src', 'agents');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(agentsDir, { recursive: true });
// Hermes resolves one complete provider for its skills+agents layout. Keep
// this legacy-marker fixture complete so it exercises marker compatibility
// without relying on forbidden per-kind provider mixing (#4132).
fs.writeFileSync(path.join(agentsDir, 'gsd-test-agent.md'), '# test agent\n');
for (const stem of stems) {
fs.writeFileSync(path.join(srcDir, `${stem}.md`), [
'---',
@@ -7583,6 +7886,11 @@ describe('#3719: real global Claude install — agents/*.md @-refs must resolve
const failures = [];
for (const file of walk(rootDir)) {
if (!file.endsWith('.md')) continue;
const relative = path.relative(claudeGlobal.configDir, file).replace(/\\/g, '/');
// The installation-owned Runtime Surface corpus is raw package input,
// not an emitted runtime artifact. It must remain byte-identical to the
// package and is validated separately by the corpus parity matrix.
if (relative.startsWith('gsd-core/commands/gsd/') || relative.startsWith('gsd-core/agents/')) continue;
const content = fs.readFileSync(file, 'utf8');
for (const line of splitLines(content)) {
if (/@\$HOME\//.test(line.replace(INLINE_CODE_SPAN_RE, ''))) failures.push({ file, line });