Files
msd-core/tests/external-descriptor-loader-wiring.test.cjs
Tom Boucher d2518e142d feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2] (#1808)
* feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2]

Phase 4 slice 2. loadRegistry({includeInstalled:true, configHome}) now rejects
(skip + warn, fail-closed) any installed third-party descriptor whose declared
destSubpath resolves outside the supplied configHome, BEFORE it is composed.

- src/capability-loader.cts: LoadRegistryOptions.configHome?:string (optional,
  backward-compatible). Require external-descriptor-trust.cjs (typed). Before
  overlayCaps.push(cap), if configHome set, assertDescriptorConfined(cap,
  configHome) — on throw, skip('configHome confinement rejected: ...') +
  continue. Fail-closed via the loader's existing per-candidate skip semantics.
- tests/external-descriptor-loader-wiring.test.cjs: integration test — escaping
  overlay skipped with confinement reason when configHome set; confined overlay
  composes; omitted configHome = no load-time check (backward-compatible).

Defense-in-depth with Phase 2: load-time rejects malformed descriptors early
(this slice); install-time assertDestWithinConfigHome bounds actual writes
(#1679 AC3). Existing capability-loader.test.cjs 54/54 (no regression —
additive optional option). Companion MCP server -> slice 3.

* chore(changeset): add Changed fragment for loadRegistry configHome confinement wiring (#1681)
2026-06-28 12:25:02 -04:00

76 lines
3.4 KiB
JavaScript

'use strict';
/**
* Integration test: loadRegistry wires the external-descriptor trust gate
* (ADR-1239 Phase C-2 / #1681 slice 2). When `configHome` is supplied, an
* installed overlay whose declared destSubpath escapes it is rejected
* (skip + confinement reason) and NOT composed; a confined overlay composes.
*/
const { 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 { cleanup } = require('./helpers.cjs');
const { loadRegistry } = require('../gsd-core/bin/lib/capability-loader.cjs');
const HOST = '1.6.0';
function featureCap(id, extra) {
return {
id, role: 'feature', version: '1.0.0', title: id, description: 'overlay cap',
tier: 'standard', requires: [], engines: { gsd: '>=1.0.0' },
runtimeCompat: { supported: ['*'], unsupported: [] },
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], gates: [],
...extra,
};
}
// Build a temp GSD home with .gsd/capabilities/<id>/capability.json per cap.
function makeOverlayHome(caps) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-trust-'));
for (const cap of caps) {
const dir = path.join(home, '.gsd', 'capabilities', cap.id);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'capability.json'), JSON.stringify(cap), 'utf8');
}
return home;
}
test('loadRegistry configHome confinement: escaping overlay is skipped with a confinement reason', () => {
const home = makeOverlayHome([
featureCap('confined-host', { runtime: { artifactLayout: { global: [{ destSubpath: 'skills' }] } } }),
featureCap('escape-host', { runtime: { artifactLayout: { global: [{ destSubpath: '../../../etc/passwd' }] } } }),
]);
try {
const reg = loadRegistry({
includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST,
configHome: path.join(home, '.target'),
});
const overlayIds = Object.keys(reg.capabilities || {}).filter((id) => id === 'confined-host' || id === 'escape-host');
assert.ok(overlayIds.includes('confined-host'), 'confined overlay must be composed');
assert.ok(!overlayIds.includes('escape-host'), 'escaping overlay must NOT be composed');
const skips = (reg._overlay && reg._overlay.warnings) || [];
const confinementSkip = skips.find((s) => /confinement/.test(s.reason || ''));
assert.ok(confinementSkip, `an overlay must be skipped with a confinement reason; warnings=${JSON.stringify(skips)}`);
assert.match(confinementSkip.reason, /escape-host/, 'the confinement skip must name the escaping descriptor');
} finally {
cleanup(home);
}
});
test('loadRegistry configHome confinement: omitted configHome = no load-time check (backward-compatible; relies on install-time gate)', () => {
// Same escaping overlay, but no configHome passed → it is NOT rejected by the load-time gate.
const home = makeOverlayHome([
featureCap('escape-host', { runtime: { artifactLayout: { global: [{ destSubpath: '../../../etc' }] } } }),
]);
try {
const reg = loadRegistry({ includeInstalled: true, gsdHome: home, cwd: home, hostVersion: HOST });
const warnings = (reg._overlay && reg._overlay.warnings) || [];
const confinementSkip = warnings.find((s) => /confinement/.test(s.reason || ''));
assert.ok(!confinementSkip, 'no configHome → no load-time confinement check (backward-compatible)');
} finally {
cleanup(home);
}
});