fix(#2086): #338 fail-safe floor for reference-host behaviors on registry-load failure

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:
Tom Boucher
2026-07-08 15:19:12 -04:00
parent ff6e928530
commit 102ffa0f9e
2 changed files with 74 additions and 3 deletions

View File

@@ -315,16 +315,46 @@ try {
_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
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
* 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) {
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
return (cap && cap.runtime && cap.runtime.hostBehaviors) || {};
return _resolveHostBehaviors(runtime, _capabilityRegistry);
}
/**
@@ -11099,6 +11129,9 @@ module.exports = {
install,
installAllRuntimes,
uninstall,
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
_resolveHostBehaviors,
FALLBACK_HOST_BEHAVIORS,
convertSlashCommandsToCodexSkillMentions,
convertClaudeCommandToCodexSkill,
convertClaudeCommandToKimiSkill,