fix(settings-validator): accept CC 2.1.114–181 keys + xhigh effort
Clears a cluster of active false positives where valid, documented Claude Code config was flagged as unknown/invalid (gap matrix, Batch 1). KNOWN_KEYS +11 (CC 2.1.133–181): allowAllClaudeAiMcps, disableBundledSkills, enforceAvailableModels, fallbackModel, footerLinksRegexes, parentSettingsBehavior, pluginSuggestionMarketplaces, requiredMaximumVersion, requiredMinimumVersion, sandbox, wheelScrollAccelerationEnabled. VALID_EFFORT_LEVELS += 'xhigh' (CC 2.1.154 Opus-4.8 top tier). TYPE_CHECKS += disableBundledSkills/wheelScrollAccelerationEnabled (boolean). fallbackModel intentionally NOT type-checked (string | array<=3). Tests: hermetic runtime temp-fixture (path-guard blocks committing settings.json); 29/29 SET green, full hermetic suite unaffected. Ref: docs/cc-2.1.x-gap-matrix.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
315ea2259f
commit
73099354c7
2 changed files with 98 additions and 21 deletions
|
|
@ -12,26 +12,29 @@ import { extractKeys } from './lib/string-utils.mjs';
|
||||||
|
|
||||||
const SCANNER = 'SET';
|
const SCANNER = 'SET';
|
||||||
|
|
||||||
/** Known top-level settings.json keys (as of April 2026) */
|
/** Known top-level settings.json keys (as of CC 2.1.181 / June 2026) */
|
||||||
const KNOWN_KEYS = new Set([
|
const KNOWN_KEYS = new Set([
|
||||||
'additionalDirectories',
|
'additionalDirectories',
|
||||||
'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers',
|
'agent', 'allowAllClaudeAiMcps', 'allowedChannelPlugins', 'allowedHttpHookUrls',
|
||||||
'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly',
|
'allowedMcpServers', 'allowManagedHooksOnly', 'allowManagedMcpServersOnly',
|
||||||
'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory',
|
'allowManagedPermissionRulesOnly', 'alwaysThinkingEnabled', 'apiKeyHelper',
|
||||||
'autoMemoryEnabled', 'autoMode', 'autoUpdatesChannel', 'availableModels',
|
'attribution', 'autoMemoryDirectory', 'autoMemoryEnabled', 'autoMode',
|
||||||
'awsAuthRefresh', 'awsCredentialExport', 'blockedMarketplaces', 'channelsEnabled',
|
'autoUpdatesChannel', 'availableModels', 'awsAuthRefresh', 'awsCredentialExport',
|
||||||
'cleanupPeriodDays', 'claudeMdExcludes', 'companyAnnouncements', 'defaultShell',
|
'blockedMarketplaces', 'channelsEnabled', 'cleanupPeriodDays', 'claudeMdExcludes',
|
||||||
'deniedMcpServers', 'disableAllHooks', 'disableAutoMode', 'disableDeepLinkRegistration',
|
'companyAnnouncements', 'defaultShell', 'deniedMcpServers', 'disableAllHooks',
|
||||||
|
'disableAutoMode', 'disableBundledSkills', 'disableDeepLinkRegistration',
|
||||||
'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers',
|
'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers',
|
||||||
'enabledMcpjsonServers', 'enabledPlugins', 'env', 'extraKnownMarketplaces',
|
'enabledMcpjsonServers', 'enabledPlugins', 'enforceAvailableModels', 'env',
|
||||||
'fastModePerSessionOptIn', 'feedbackSurveyRate', 'fileSuggestion',
|
'extraKnownMarketplaces', 'fallbackModel', 'fastModePerSessionOptIn',
|
||||||
'forceLoginMethod', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars',
|
'feedbackSurveyRate', 'fileSuggestion', 'footerLinksRegexes', 'forceLoginMethod',
|
||||||
'includeCoAuthoredBy', 'includeGitInstructions', 'language', 'model',
|
'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars', 'includeCoAuthoredBy',
|
||||||
'modelOverrides', 'otelHeadersHelper', 'outputStyle', 'permissions',
|
'includeGitInstructions', 'language', 'model', 'modelOverrides', 'otelHeadersHelper',
|
||||||
'plansDirectory', 'pluginTrustMessage', 'prefersReducedMotion',
|
'outputStyle', 'parentSettingsBehavior', 'permissions', 'plansDirectory',
|
||||||
'respectGitignore', 'showClearContextOnPlanAccept', 'showThinkingSummaries',
|
'pluginSuggestionMarketplaces', 'pluginTrustMessage', 'prefersReducedMotion',
|
||||||
'spinnerTipsEnabled', 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine',
|
'requiredMaximumVersion', 'requiredMinimumVersion', 'respectGitignore', 'sandbox',
|
||||||
'strictKnownMarketplaces', 'useAutoModeDuringPlan', 'voiceEnabled',
|
'showClearContextOnPlanAccept', 'showThinkingSummaries', 'spinnerTipsEnabled',
|
||||||
|
'spinnerTipsOverride', 'spinnerVerbs', 'statusLine', 'strictKnownMarketplaces',
|
||||||
|
'useAutoModeDuringPlan', 'voiceEnabled', 'wheelScrollAccelerationEnabled',
|
||||||
'worktree', '$schema',
|
'worktree', '$schema',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -47,6 +50,7 @@ const TYPE_CHECKS = new Map([
|
||||||
['channelsEnabled', 'boolean'],
|
['channelsEnabled', 'boolean'],
|
||||||
['cleanupPeriodDays', 'number'],
|
['cleanupPeriodDays', 'number'],
|
||||||
['disableAllHooks', 'boolean'],
|
['disableAllHooks', 'boolean'],
|
||||||
|
['disableBundledSkills', 'boolean'],
|
||||||
['effortLevel', 'string'],
|
['effortLevel', 'string'],
|
||||||
['enableAllProjectMcpServers', 'boolean'],
|
['enableAllProjectMcpServers', 'boolean'],
|
||||||
['fastModePerSessionOptIn', 'boolean'],
|
['fastModePerSessionOptIn', 'boolean'],
|
||||||
|
|
@ -60,10 +64,11 @@ const TYPE_CHECKS = new Map([
|
||||||
['showThinkingSummaries', 'boolean'],
|
['showThinkingSummaries', 'boolean'],
|
||||||
['spinnerTipsEnabled', 'boolean'],
|
['spinnerTipsEnabled', 'boolean'],
|
||||||
['voiceEnabled', 'boolean'],
|
['voiceEnabled', 'boolean'],
|
||||||
|
['wheelScrollAccelerationEnabled', 'boolean'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** Valid effortLevel values */
|
/** Valid effortLevel values (CC 2.1.154 added 'xhigh' as the Opus-4.8 top tier) */
|
||||||
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']);
|
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
||||||
|
|
||||||
/** v5 M6: warn when additionalDirectories grows beyond this — each entry adds
|
/** v5 M6: warn when additionalDirectories grows beyond this — each entry adds
|
||||||
* a project root to walks/discovery, inflating per-turn cost and confusing scope. */
|
* a project root to walks/discovery, inflating per-turn cost and confusing scope. */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { describe, it, beforeEach } from 'node:test';
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { resolve } from 'node:path';
|
import { resolve, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||||
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';
|
||||||
import { scan } from '../../scanners/settings-validator.mjs';
|
import { scan } from '../../scanners/settings-validator.mjs';
|
||||||
|
|
@ -119,6 +121,76 @@ describe('SET scanner — additionalDirectories (v5 M6)', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('SET scanner — CC 2.1.114→181 valid keys (Batch 1 false-positive fix)', () => {
|
||||||
|
// The pre-write path-guard blocks the agent from committing settings.json
|
||||||
|
// fixtures, so this suite materializes a hermetic temp fixture at runtime.
|
||||||
|
let tmpRoot;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
const NEW_KEYS = [
|
||||||
|
'sandbox', 'fallbackModel', 'enforceAvailableModels', 'disableBundledSkills',
|
||||||
|
'pluginSuggestionMarketplaces', 'requiredMinimumVersion', 'requiredMaximumVersion',
|
||||||
|
'allowAllClaudeAiMcps', 'footerLinksRegexes', 'wheelScrollAccelerationEnabled',
|
||||||
|
'parentSettingsBehavior',
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
resetCounter();
|
||||||
|
tmpRoot = await mkdtemp(join(tmpdir(), 'ca-set-2181-'));
|
||||||
|
await mkdir(join(tmpRoot, '.claude'), { recursive: true });
|
||||||
|
const settings = {
|
||||||
|
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
||||||
|
effortLevel: 'xhigh', // CC 2.1.154
|
||||||
|
sandbox: { allowAppleEvents: false }, // CC 2.1.181
|
||||||
|
fallbackModel: ['claude-opus-4-8', 'claude-sonnet-4-6'], // CC 2.1.166 (string|array)
|
||||||
|
enforceAvailableModels: true, // CC 2.1.175
|
||||||
|
disableBundledSkills: true, // CC 2.1.169
|
||||||
|
pluginSuggestionMarketplaces: ['acme'], // CC 2.1.152
|
||||||
|
requiredMinimumVersion: '2.1.114', // CC 2.1.163
|
||||||
|
requiredMaximumVersion: '2.2.0', // CC 2.1.163
|
||||||
|
allowAllClaudeAiMcps: false, // CC 2.1.149
|
||||||
|
footerLinksRegexes: ['^https://internal\\.'], // CC 2.1.181
|
||||||
|
wheelScrollAccelerationEnabled: true, // CC 2.1.174
|
||||||
|
parentSettingsBehavior: 'merge', // CC 2.1.133
|
||||||
|
permissions: { deny: ['Read(./.env)'], allow: ['Bash(npm run *)'] },
|
||||||
|
};
|
||||||
|
await writeFile(
|
||||||
|
join(tmpRoot, '.claude', 'settings.json'),
|
||||||
|
JSON.stringify(settings, null, 2) + '\n',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const discovery = await discoverConfigFiles(tmpRoot);
|
||||||
|
result = await scan(tmpRoot, discovery);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts effortLevel "xhigh" (CC 2.1.154)', () => {
|
||||||
|
const bad = result.findings.find(f => /xhigh/.test(f.evidence || ''));
|
||||||
|
assert.equal(bad, undefined, 'xhigh must be a valid effortLevel');
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const key of NEW_KEYS) {
|
||||||
|
it(`does NOT flag "${key}" as an unknown key`, () => {
|
||||||
|
const unknown = result.findings.find(f =>
|
||||||
|
f.title === 'Unknown settings key' && f.evidence === key);
|
||||||
|
assert.equal(unknown, undefined, `${key} should be in KNOWN_KEYS`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('treats fallbackModel array value without a single-type mismatch', () => {
|
||||||
|
const f = result.findings.find(x => /fallbackModel/.test(x.evidence || ''));
|
||||||
|
assert.equal(f, undefined, 'fallbackModel (string|array) must not be type-checked');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces zero findings for an all-valid CC 2.1.181 settings file', () => {
|
||||||
|
assert.equal(result.findings.length, 0,
|
||||||
|
`expected clean scan; got: ${result.findings.map(f => `${f.title}:${f.evidence || ''}`).join(' | ')}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('SET scanner — empty project', () => {
|
describe('SET scanner — empty project', () => {
|
||||||
let result;
|
let result;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue