ADR-1244 Phase 5 (D7). dispatchOverlayCapabilityCommand in gsd-tools.cjs dispatches an installed third-party capability command family via loadRegistry({includeInstalled}), gated on a committed ledger entry (consent) and confined to the capability's install root (defaultRequireFromInstallRoot: bare-.cjs basename + realpath containment, rejects ../ traversal + symlink escape); same own-property/function/sync/ExitError guards as the first-party path. capability-loader records _overlay.commandRoots only for accepted overlay caps with a committed, structurally-valid ledger entry (fail closed). First-party graphify/intel/audit unchanged (already on the registry seam). 3 Codex rounds converged + /security-review (no HIGH) + /code-review (Approve); gsd-test green both platforms; CI green.
Closes #1434.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,16 @@
|
||||
|
||||
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 { dispatchCapabilityCommand } = require('../gsd-core/bin/gsd-tools.cjs');
|
||||
const {
|
||||
dispatchCapabilityCommand,
|
||||
dispatchOverlayCapabilityCommand,
|
||||
defaultRequireFromInstallRoot,
|
||||
} = require('../gsd-core/bin/gsd-tools.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -776,3 +784,236 @@ describe('dispatchCapabilityCommand — real registry behavior-preservation', ()
|
||||
assert.strictEqual(result, false, 'unknown command against real registry must return false');
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ADR-1244 Phase 5 (D7) — third-party overlay command dispatch
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Synthetic overlay registry: commandFamilies + _overlay.commandRoots (capId → install dir). */
|
||||
function makeOverlayRegistry(families, commandRoots) {
|
||||
return { commandFamilies: families, _overlay: { warnings: [], incompatibleGateCapIds: [], blockedGates: [], commandRoots } };
|
||||
}
|
||||
|
||||
describe('dispatchOverlayCapabilityCommand — third-party overlay (Phase 5)', () => {
|
||||
test('happy path: a third-party family (capId in commandRoots) dispatches from its install root', () => {
|
||||
const calls = [];
|
||||
const loadRegistry = () => makeOverlayRegistry(
|
||||
{ mycmd: { capId: 'thirdparty', module: 'router.cjs', router: 'run' } },
|
||||
{ thirdparty: '/install/root/thirdparty' },
|
||||
);
|
||||
const requireModule = (installRoot, m) => {
|
||||
assert.strictEqual(installRoot, '/install/root/thirdparty', 'module required FROM the install root');
|
||||
assert.strictEqual(m, 'router.cjs');
|
||||
return { run: (ctx) => calls.push(ctx) };
|
||||
};
|
||||
const result = dispatchOverlayCapabilityCommand({
|
||||
command: 'mycmd', args: ['a'], cwd: '/p', raw: false, error: () => {}, loadRegistry, requireModule,
|
||||
});
|
||||
assert.strictEqual(result, true);
|
||||
assert.strictEqual(calls.length, 1);
|
||||
assert.deepEqual(calls[0].args, ['a']);
|
||||
});
|
||||
|
||||
test('a FIRST-PARTY family (capId NOT in commandRoots) falls through (handled by frozen-registry dispatch)', () => {
|
||||
let required = false;
|
||||
const loadRegistry = () => makeOverlayRegistry(
|
||||
{ graphify: { capId: 'graphify', module: 'graphify-command-router.cjs', router: 'routeGraphifyCommand' } },
|
||||
{}, // graphify is first-party → not in commandRoots
|
||||
);
|
||||
const result = dispatchOverlayCapabilityCommand({
|
||||
command: 'graphify', args: [], cwd: '/p', raw: false, error: () => {},
|
||||
loadRegistry, requireModule: () => { required = true; return {}; },
|
||||
});
|
||||
assert.strictEqual(result, false, 'first-party must fall through, not be dispatched as overlay');
|
||||
assert.strictEqual(required, false, 'first-party module must NOT be required from an install root');
|
||||
});
|
||||
|
||||
test('unknown family → false', () => {
|
||||
const loadRegistry = () => makeOverlayRegistry({}, {});
|
||||
assert.strictEqual(dispatchOverlayCapabilityCommand({ command: 'nope', args: [], cwd: '/p', raw: false, error: () => {}, loadRegistry, requireModule: () => ({}) }), false);
|
||||
});
|
||||
|
||||
test('no _overlay / no commandRoots on the registry → false', () => {
|
||||
assert.strictEqual(dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: () => {}, loadRegistry: () => ({ commandFamilies: { x: { capId: 'x', module: 'm.cjs', router: 'r' } } }), requireModule: () => ({}) }), false);
|
||||
});
|
||||
|
||||
test('loadRegistry throwing → false (falls through to Unknown)', () => {
|
||||
assert.strictEqual(dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: () => {}, loadRegistry: () => { throw new Error('overlay scan failed'); }, requireModule: () => ({}) }), false);
|
||||
});
|
||||
|
||||
test('prototype-pollution command keys → false (never reach the registry)', () => {
|
||||
for (const command of ['__proto__', 'constructor', 'prototype']) {
|
||||
let loaded = false;
|
||||
const r = dispatchOverlayCapabilityCommand({ command, args: [], cwd: '/p', raw: false, error: () => {}, loadRegistry: () => { loaded = true; return makeOverlayRegistry({}, {}); }, requireModule: () => ({}) });
|
||||
assert.strictEqual(r, false);
|
||||
assert.strictEqual(loaded, false, command + ' must short-circuit before loadRegistry');
|
||||
}
|
||||
});
|
||||
|
||||
test('CONSENT NEGATIVE PROOF: a family whose capId is absent from commandRoots is never require()d', () => {
|
||||
// Models an unconsented/_pending cap: the loader excludes it from commandRoots, so even though
|
||||
// the (synthetic) commandFamilies names it, dispatch must NOT load its module.
|
||||
let required = false;
|
||||
const loadRegistry = () => makeOverlayRegistry(
|
||||
{ evil: { capId: 'evil', module: 'evil.cjs', router: 'run' } },
|
||||
{}, // 'evil' NOT consented → absent from commandRoots
|
||||
);
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'evil', args: [], cwd: '/p', raw: false, error: () => {}, loadRegistry, requireModule: () => { required = true; return { run() {} }; } });
|
||||
assert.strictEqual(result, false);
|
||||
assert.strictEqual(required, false, 'an unconsented capability module must never be required');
|
||||
});
|
||||
|
||||
test('module load failure → error diagnostic + consumed (true)', () => {
|
||||
const errs = [];
|
||||
const loadRegistry = () => makeOverlayRegistry({ x: { capId: 'tp', module: 'm.cjs', router: 'r' } }, { tp: '/root' });
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: (m) => errs.push(m), loadRegistry, requireModule: () => { throw new Error('boom'); } });
|
||||
assert.strictEqual(result, true);
|
||||
assert.ok(errs.some((e) => /failed to load from its install root/.test(e)));
|
||||
});
|
||||
|
||||
test('router not an own export → error + consumed', () => {
|
||||
const errs = [];
|
||||
const loadRegistry = () => makeOverlayRegistry({ x: { capId: 'tp', module: 'm.cjs', router: 'toString' } }, { tp: '/root' });
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: (m) => errs.push(m), loadRegistry, requireModule: () => ({}) });
|
||||
assert.strictEqual(result, true);
|
||||
assert.ok(errs.some((e) => /is not an own export/.test(e)));
|
||||
});
|
||||
|
||||
test('router not a function → error + consumed', () => {
|
||||
const errs = [];
|
||||
const loadRegistry = () => makeOverlayRegistry({ x: { capId: 'tp', module: 'm.cjs', router: 'r' } }, { tp: '/root' });
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: (m) => errs.push(m), loadRegistry, requireModule: () => ({ r: 42 }) });
|
||||
assert.strictEqual(result, true);
|
||||
assert.ok(errs.some((e) => /is not a function/.test(e)));
|
||||
});
|
||||
|
||||
test('async router (returns a Promise) → SDK fail-fast diagnostic', () => {
|
||||
const errs = [];
|
||||
const loadRegistry = () => makeOverlayRegistry({ x: { capId: 'tp', module: 'm.cjs', router: 'r' } }, { tp: '/root' });
|
||||
dispatchOverlayCapabilityCommand({ command: 'x', args: [], cwd: '/p', raw: false, error: (m) => errs.push(m), loadRegistry, requireModule: () => ({ r: () => Promise.resolve() }) });
|
||||
assert.ok(errs.some((e) => /must be synchronous/.test(e)));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── defaultRequireFromInstallRoot — real-filesystem confinement (negative proof) ───
|
||||
|
||||
describe('defaultRequireFromInstallRoot — install-root confinement (Phase 5)', () => {
|
||||
const dirs = [];
|
||||
const mkroot = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-disp-')); dirs.push(d); return d; };
|
||||
test.after(() => { for (const d of dirs) cleanup(d); });
|
||||
|
||||
test('loads a bare .cjs module that lives inside the install root', () => {
|
||||
const root = mkroot();
|
||||
fs.writeFileSync(path.join(root, 'router.cjs'), 'module.exports = { run: () => 7 };', 'utf8');
|
||||
const mod = defaultRequireFromInstallRoot(root, 'router.cjs');
|
||||
assert.strictEqual(mod.run(), 7);
|
||||
});
|
||||
|
||||
test('rejects a non-.cjs / path-separator / .. module name', () => {
|
||||
const root = mkroot();
|
||||
assert.throws(() => defaultRequireFromInstallRoot(root, 'router.js'), /bare \.cjs basename/);
|
||||
assert.throws(() => defaultRequireFromInstallRoot(root, '../escape.cjs'), /bare \.cjs basename/);
|
||||
assert.throws(() => defaultRequireFromInstallRoot(root, 'sub/router.cjs'), /bare \.cjs basename/);
|
||||
assert.throws(() => defaultRequireFromInstallRoot(root, '/abs/router.cjs'), /bare \.cjs basename/);
|
||||
});
|
||||
|
||||
test('NEGATIVE PROOF: a symlinked module pointing OUTSIDE the install root is not loaded', () => {
|
||||
const root = mkroot();
|
||||
const outside = mkroot();
|
||||
const secret = path.join(outside, 'secret.cjs');
|
||||
fs.writeFileSync(secret, 'module.exports = { run: () => "PWNED" };', 'utf8');
|
||||
// A bare-basename symlink inside the root whose real target escapes the root.
|
||||
let linked = true;
|
||||
try { fs.symlinkSync(secret, path.join(root, 'router.cjs')); } catch { linked = false; }
|
||||
if (!linked) return; // platform without symlink perms — skip
|
||||
assert.throws(() => defaultRequireFromInstallRoot(root, 'router.cjs'), /outside its install root/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── End-to-end: real loadRegistry + real require + real ledger (consent + confinement) ───
|
||||
|
||||
describe('dispatchOverlayCapabilityCommand — end-to-end (real loadRegistry, real require, real ledger)', () => {
|
||||
const homes = [];
|
||||
let savedGsdHome;
|
||||
const mkhome = () => { const h = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-e2e-')); homes.push(h); return h; };
|
||||
|
||||
function validCap(id, family) {
|
||||
return {
|
||||
id, role: 'feature', version: '1.0.0', title: id, description: 'e2e cap', tier: 'standard',
|
||||
requires: [], engines: { gsd: '>=1.0.0' }, runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [],
|
||||
commands: [{ family, module: 'router.cjs', router: 'run' }],
|
||||
};
|
||||
}
|
||||
// The router writes a marker file so "did it execute?" is a filesystem fact (negative proof).
|
||||
const ROUTER_BODY = "module.exports = { run: (ctx) => { require('fs').writeFileSync(require('path').join(ctx.cwd, 'RAN.txt'), String((ctx.args||[]).join(','))); } };";
|
||||
|
||||
function placeBundle(home, id, family, { committed }) {
|
||||
const dir = path.join(home, '.gsd', 'capabilities', id);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(validCap(id, family)), 'utf8');
|
||||
fs.writeFileSync(path.join(dir, 'router.cjs'), ROUTER_BODY, 'utf8');
|
||||
if (committed) {
|
||||
fs.writeFileSync(
|
||||
path.join(home, '.gsd-capabilities.json'),
|
||||
JSON.stringify({ version: '1', updatedAt: 'x', entries: { [id]: { id, version: '1.0.0', source: 's', integrity: '', files: [], sharedEdits: [] } } }),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test.beforeEach(() => { savedGsdHome = process.env.GSD_HOME; });
|
||||
test.afterEach(() => { if (savedGsdHome === undefined) delete process.env.GSD_HOME; else process.env.GSD_HOME = savedGsdHome; });
|
||||
test.after(() => { for (const h of homes) cleanup(h); });
|
||||
|
||||
test('a COMMITTED (consented) third-party command runs, from its install root', () => {
|
||||
const home = mkhome();
|
||||
placeBundle(home, 'e2ecap', 'e2e-cmd', { committed: true });
|
||||
process.env.GSD_HOME = home; // global overlay scope = home/.gsd/capabilities
|
||||
const errs = [];
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'e2e-cmd', args: ['hello'], cwd: home, raw: false, error: (m) => errs.push(m) });
|
||||
assert.strictEqual(result, true, 'consented command consumed: ' + JSON.stringify(errs));
|
||||
assert.strictEqual(fs.readFileSync(path.join(home, 'RAN.txt'), 'utf8'), 'hello', 'router executed with forwarded args');
|
||||
});
|
||||
|
||||
test('NEGATIVE PROOF: a dropped bundle with NO ledger entry is never dispatched / never executes', () => {
|
||||
const home = mkhome();
|
||||
placeBundle(home, 'evilcap', 'evil-cmd', { committed: false }); // bundle on disk, NO ledger
|
||||
process.env.GSD_HOME = home;
|
||||
const result = dispatchOverlayCapabilityCommand({ command: 'evil-cmd', args: ['x'], cwd: home, raw: false, error: () => {} });
|
||||
assert.strictEqual(result, false, 'unconsented family must fall through to Unknown');
|
||||
assert.strictEqual(fs.existsSync(path.join(home, 'RAN.txt')), false, 'the dropped module must NEVER execute');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Overlay router error semantics (parity with the first-party path) ───
|
||||
|
||||
describe('dispatchOverlayCapabilityCommand — router error semantics', () => {
|
||||
function overlayReg() {
|
||||
return { commandFamilies: { x: { capId: 'tp', module: 'm.cjs', router: 'run' } }, _overlay: { warnings: [], incompatibleGateCapIds: [], blockedGates: [], commandRoots: { tp: '/root' } } };
|
||||
}
|
||||
|
||||
test('overlay router throwing an ExitError → propagates unchanged, error() NOT called', () => {
|
||||
const thrown = new ExitError(1, 'intentional-exit');
|
||||
const errs = [];
|
||||
let caught;
|
||||
try {
|
||||
dispatchOverlayCapabilityCommand({
|
||||
command: 'x', args: [], cwd: '/p', raw: false, error: (m) => errs.push(m),
|
||||
loadRegistry: overlayReg, requireModule: () => ({ run: () => { throw thrown; } }),
|
||||
});
|
||||
} catch (e) { caught = e; }
|
||||
assert.strictEqual(caught, thrown, 'the original ExitError must propagate unchanged');
|
||||
assert.strictEqual(errs.length, 0, 'error() must not be called when an ExitError propagates');
|
||||
});
|
||||
|
||||
test('overlay router throwing a generic Error → attributed error() + consumed (true)', () => {
|
||||
const errs = [];
|
||||
const result = dispatchOverlayCapabilityCommand({
|
||||
command: 'x', args: [], cwd: '/p', raw: false, error: (m, reason) => errs.push({ m, reason }),
|
||||
loadRegistry: overlayReg, requireModule: () => ({ run: () => { throw new Error('kaboom'); } }),
|
||||
});
|
||||
assert.strictEqual(result, true, 'consumed');
|
||||
assert.ok(errs.some((e) => /threw: kaboom/.test(e.m)), 'router throw attributed to the command');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user