feat(shell-projection): add shim wrapper drift guard (#3448)

* feat(shell-projection): add shim wrapper drift guard

* chore(changeset): add PR 3448 changelog fragment
This commit is contained in:
Tom Boucher
2026-05-12 20:49:43 -04:00
committed by GitHub
parent bf3c736029
commit 0bcc9146a4
3 changed files with 145 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 3448
---
**Installer shim/wrapper drift guard now enforces projection seam ownership** — inline shim text builders in installer-owned serialized shell-command surfaces are rejected unless routed through the Shell Command Projection Module.

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env node
'use strict';
/**
* Focused drift guard for issue #3442:
* prevent installer-owned inline shim/wrapper text builders from bypassing the
* Shell Command Projection Module seam.
*
* Scope intentionally excludes subprocess execution helpers (spawnSync /
* execFileSync) because those are safe internal execution primitives, not
* serialized shell-command rendering.
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const targetArg = process.argv[2] || path.join(ROOT, 'bin', 'install.js');
const target = path.resolve(targetArg);
const rel = path.relative(ROOT, target);
let content;
try {
content = fs.readFileSync(target, 'utf8');
} catch (error) {
process.stderr.write(`lint-shell-command-projection-drift: failed to read ${target}: ${error.message}\n`);
process.exit(1);
}
const forbidden = [
{
label: 'inline cmd shim builder',
pattern: /@ECHO OFF\\r\\n@SETLOCAL\\r\\n@node /,
},
{
label: 'inline pwsh shim builder',
pattern: /#!\/usr\/bin\/env pwsh\\n& node /,
},
{
label: 'inline sh shim builder',
pattern: /#!\/usr\/bin\/env sh\\nexec node /,
},
];
const matches = forbidden.filter((rule) => rule.pattern.test(content));
if (matches.length === 0) {
process.stdout.write(`ok shell-projection-drift: ${rel}\n`);
process.exit(0);
}
process.stderr.write(`ERROR shell-projection-drift: inline serialized shim builders found in ${rel}\n`);
for (const match of matches) {
process.stderr.write(` - ${match.label}\n`);
}
process.stderr.write('Route shim/wrapper rendering through get-shit-done/bin/lib/shell-command-projection.cjs\n');
process.stderr.write('Safe subprocess execution via spawnSync/execFileSync is intentionally allowed.\n');
process.exit(1);

View File

@@ -0,0 +1,83 @@
'use strict';
process.env.GSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const ROOT = path.resolve(__dirname, '..');
const INSTALL = require(path.join(ROOT, 'bin', 'install.js'));
const PROJECTION = require(path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'shell-command-projection.cjs'));
const DRIFT_LINT = path.join(ROOT, 'scripts', 'lint-shell-command-projection-drift.cjs');
function runLint(targetFile) {
return spawnSync(process.execPath, [DRIFT_LINT, targetFile], {
cwd: ROOT,
encoding: 'utf8',
});
}
describe('bug #3442: shim/wrapper projection seam', () => {
test('buildWindowsShimTriple matches shared projection output', () => {
const shimSrc = path.join(ROOT, 'bin', 'gsd-sdk.js');
const fromInstall = INSTALL.buildWindowsShimTriple(shimSrc);
const fromProjection = PROJECTION.buildWindowsShimTriple(shimSrc);
assert.deepEqual(fromInstall.invocation, fromProjection.invocation);
assert.deepEqual(fromInstall.eol, fromProjection.eol);
assert.deepEqual(fromInstall.fileNames, fromProjection.fileNames);
assert.equal(fromInstall.render.cmd(), fromProjection.render.cmd());
assert.equal(fromInstall.render.ps1(), fromProjection.render.ps1());
assert.equal(fromInstall.render.sh(), fromProjection.render.sh());
});
});
describe('bug #3442: shim/wrapper serialized-command drift guard', () => {
test('drift guard passes for current install.js', () => {
const result = runLint(path.join(ROOT, 'bin', 'install.js'));
assert.equal(result.status, 0, `expected lint pass, got:\n${result.stderr || result.stdout}`);
});
test('drift guard fails when install-owned inline shim text builder is present', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-'));
try {
const fixture = path.join(tmp, 'install-inline-builder.js');
fs.writeFileSync(
fixture,
[
'function badBuilder() {',
" return '@ECHO OFF\\r\\n@SETLOCAL\\r\\n@node \"C:/shim.js\" %*\\r\\n';",
'}',
'',
].join('\n'),
);
const result = runLint(fixture);
assert.notEqual(result.status, 0, 'inline shim renderer should be rejected by the drift guard');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('drift guard does not block safe subprocess execution patterns', () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3442-'));
try {
const fixture = path.join(tmp, 'install-subprocess-safe.js');
fs.writeFileSync(
fixture,
[
"const cp = require('node:child_process');",
"cp.spawnSync('cmd.exe', ['/c', 'echo ok']);",
"cp.execFileSync('bash', ['-lc', 'printf %s \"$PATH\"']);",
'',
].join('\n'),
);
const result = runLint(fixture);
assert.equal(result.status, 0, `spawnSync/execFileSync should remain allowed:\n${result.stderr || result.stdout}`);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});