Reviewer (PR #2106, elevated): if capability-registry.cjs fails to load, _hostBehaviors('claude') returned {} — silently routing a claude LOCAL install to the repo-shared settings.json instead of the gitignored settings.local.json (#338), skipping mergeClaudePermissions + the .gsd-source marker. The migration is what introduced that registry dependency (pre-PR the path had none). Add FALLBACK_HOST_BEHAVIORS (keyed by runtime id — a data lookup, not a runtime==='claude' branch) mirroring the reference host's #338-privacy-critical keys (settingsFileByScope, permissionsSchema, sourceMarkerFile), consulted only when the registry (or the descriptor) is unavailable. Behavior degrades CLOSED, never open; the live descriptor stays the source of truth. Normal (registry-present) output is unchanged (golden parity preserved). Pinned by tests via a registry-injected _resolveHostBehaviors helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -315,16 +315,46 @@ try {
|
|||||||
_capabilityRegistry = undefined;
|
_capabilityRegistry = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
|
||||||
|
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
|
||||||
|
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
|
||||||
|
// {} and silently route a claude LOCAL install to the repo-shared, committed
|
||||||
|
// `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
|
||||||
|
// engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
|
||||||
|
// `runtime === 'claude'` branch) so behavior degrades CLOSED (safe), never open.
|
||||||
|
// The live descriptor (capabilities/claude/capability.json) remains the source of
|
||||||
|
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
|
||||||
|
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
|
||||||
|
claude: Object.freeze({
|
||||||
|
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
|
||||||
|
permissionsSchema: 'claude',
|
||||||
|
sourceMarkerFile: '.gsd-source',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a runtime's host behaviors from a capability registry, with the
|
||||||
|
* #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
|
||||||
|
* is unavailable. Registry is passed in so this is unit-testable under a
|
||||||
|
* simulated registry-load failure. (ADR-1239 / #2086)
|
||||||
|
*/
|
||||||
|
function _resolveHostBehaviors(runtime, registry) {
|
||||||
|
const cap = registry && registry.runtimes && registry.runtimes[runtime];
|
||||||
|
const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
|
||||||
|
if (declared) return declared;
|
||||||
|
return FALLBACK_HOST_BEHAVIORS[runtime] || {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Host-specific install behaviors, declared on the runtime descriptor
|
* Host-specific install behaviors, declared on the runtime descriptor
|
||||||
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
|
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
|
||||||
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
|
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
|
||||||
* for runtimes that declare none, so every behavior branch degrades to the
|
* for runtimes that declare none, so every behavior branch degrades to the
|
||||||
* generic path by default.
|
* generic path by default — EXCEPT the reference host's #338-critical keys, which
|
||||||
|
* fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
|
||||||
*/
|
*/
|
||||||
function _hostBehaviors(runtime) {
|
function _hostBehaviors(runtime) {
|
||||||
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
|
return _resolveHostBehaviors(runtime, _capabilityRegistry);
|
||||||
return (cap && cap.runtime && cap.runtime.hostBehaviors) || {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11099,6 +11129,9 @@ module.exports = {
|
|||||||
install,
|
install,
|
||||||
installAllRuntimes,
|
installAllRuntimes,
|
||||||
uninstall,
|
uninstall,
|
||||||
|
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
|
||||||
|
_resolveHostBehaviors,
|
||||||
|
FALLBACK_HOST_BEHAVIORS,
|
||||||
convertSlashCommandsToCodexSkillMentions,
|
convertSlashCommandsToCodexSkillMentions,
|
||||||
convertClaudeCommandToCodexSkill,
|
convertClaudeCommandToCodexSkill,
|
||||||
convertClaudeCommandToKimiSkill,
|
convertClaudeCommandToKimiSkill,
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ const CLAUDE_CAP = JSON.parse(
|
|||||||
);
|
);
|
||||||
const CLAUDE_AXES = CLAUDE_CAP.runtime.hostIntegration;
|
const CLAUDE_AXES = CLAUDE_CAP.runtime.hostIntegration;
|
||||||
|
|
||||||
|
// Requiring the installer (not as main) never runs the CLI; GSD_TEST_MODE is set
|
||||||
|
// defensively to match the install-test convention.
|
||||||
|
process.env.GSD_TEST_MODE = process.env.GSD_TEST_MODE || '1';
|
||||||
|
const installMod = require('../bin/install.js');
|
||||||
|
|
||||||
// -- AC2: driven through the public interface (imperative adapter) -----------
|
// -- AC2: driven through the public interface (imperative adapter) -----------
|
||||||
|
|
||||||
test('createImperativeAdapter classifies claude as imperative + composes the registry', () => {
|
test('createImperativeAdapter classifies claude as imperative + composes the registry', () => {
|
||||||
@@ -136,3 +141,36 @@ test('bin/install.js contains no `runtime === "claude"` / `runtime !== "claude"`
|
|||||||
`AC2: every hardcoded runtime==='claude'/!=='claude' branch must be descriptor-driven; found: ${offenders.join(', ')}`,
|
`AC2: every hardcoded runtime==='claude'/!=='claude' branch must be descriptor-driven; found: ${offenders.join(', ')}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -- Reviewer #2106 (elevated): #338 privacy fail-safe on registry-load failure --
|
||||||
|
// If the first-party capability registry fails to load, `_hostBehaviors('claude')`
|
||||||
|
// would return {} and route a claude LOCAL install to the repo-shared settings.json
|
||||||
|
// instead of the gitignored settings.local.json — silently reintroducing #338. The
|
||||||
|
// reference host must degrade CLOSED (safe) for its privacy-critical keys.
|
||||||
|
|
||||||
|
test('claude #338-critical host behaviors degrade CLOSED when the capability registry cannot load', () => {
|
||||||
|
// Simulate a broken bundle: registry is undefined.
|
||||||
|
const degraded = installMod._resolveHostBehaviors('claude', undefined);
|
||||||
|
assert.equal(degraded.settingsFileByScope.local, 'settings.local.json',
|
||||||
|
'#338: a claude LOCAL install must still route to the gitignored settings.local.json');
|
||||||
|
assert.equal(degraded.settingsFileByScope.global, 'settings.json');
|
||||||
|
assert.equal(degraded.permissionsSchema, 'claude', 'permission cleanup/merge must still apply');
|
||||||
|
assert.equal(degraded.sourceMarkerFile, '.gsd-source');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with the registry present, claude host behaviors come from the live descriptor (superset of the fail-safe floor)', () => {
|
||||||
|
const reg = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||||
|
const declared = installMod._resolveHostBehaviors('claude', reg);
|
||||||
|
assert.equal(declared.settingsFileByScope.local, 'settings.local.json');
|
||||||
|
assert.equal(declared.localInstallStyle, 'legacy-flat');
|
||||||
|
assert.equal(declared.authorsCanonicalWorkflow, true);
|
||||||
|
// The fail-safe floor is a strict subset of what the descriptor declares.
|
||||||
|
for (const k of Object.keys(installMod.FALLBACK_HOST_BEHAVIORS.claude)) {
|
||||||
|
assert.ok(k in declared, `descriptor must still declare the #338-critical key '${k}'`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a non-reference runtime has no fail-safe fallback (degrades to the generic path)', () => {
|
||||||
|
assert.deepEqual(installMod._resolveHostBehaviors('opencode', undefined), {});
|
||||||
|
assert.deepEqual(installMod._resolveHostBehaviors('codex', undefined), {});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user