fix(config): align SDK runtime-state key validation with CJS

This commit is contained in:
Tom Boucher
2026-05-06 15:19:34 -04:00
parent 9ae4426ebb
commit a1a81eec90
5 changed files with 42 additions and 11 deletions

View File

@@ -309,8 +309,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
pre-existing sentinel force-removes the orphan worktree before starting fresh, making
the agent self-healing across crashes. (#2839)
### Fixed
- **`config-set resolve_model_ids` no longer rejected** — `resolve_model_ids` was
documented in CONFIGURATION.md and read by model-resolution paths, but missing from
the CJS/SDK `VALID_CONFIG_KEYS` allowlists. Added to both. (#3162)

View File

@@ -23,7 +23,7 @@ import { homedir } from 'node:os';
import { join } from 'node:path';
import { GSDError, ErrorClassification } from '../errors.js';
import { VALID_PROFILES, getAgentToModelMapForProfile } from './config-query.js';
import { VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS } from './config-schema.js';
import { VALID_CONFIG_KEYS, RUNTIME_STATE_KEYS, DYNAMIC_KEY_PATTERNS } from './config-schema.js';
import { planningPaths } from './helpers.js';
import { acquireStateLock, releaseStateLock } from './state-mutation.js';
import { maskIfSecret } from './secrets.js';
@@ -86,6 +86,7 @@ const CONFIG_KEY_SUGGESTIONS: Record<string, string> = {
*/
export function isValidConfigKey(keyPath: string): { valid: boolean; suggestion?: string } {
if (VALID_CONFIG_KEYS.has(keyPath)) return { valid: true };
if (RUNTIME_STATE_KEYS.has(keyPath)) return { valid: true };
// Dynamic patterns — all sourced from shared config-schema (#2653).
// Covers agent_skills.*, review.models.*, features.*,

View File

@@ -75,6 +75,14 @@ export const VALID_CONFIG_KEYS: ReadonlySet<string> = new Set([
'resolve_model_ids',
]);
/**
* Internal runtime-state keys accepted by config-set workflows but not exposed
* as user-facing config options.
*/
export const RUNTIME_STATE_KEYS: ReadonlySet<string> = new Set([
'workflow._auto_chain_active',
]);
/**
* Dynamic-pattern validators — keys matching these regexes are also accepted.
* Each entry's `source` MUST equal the corresponding CJS regex `.source`
@@ -127,8 +135,9 @@ export const DYNAMIC_KEY_PATTERNS: readonly DynamicKeyPattern[] = [
},
];
/** Returns true if keyPath is a valid config key (exact or dynamic pattern). */
/** Returns true if keyPath is a valid config key (exact, runtime-state, or dynamic pattern). */
export function isValidConfigKeyPath(keyPath: string): boolean {
if (VALID_CONFIG_KEYS.has(keyPath)) return true;
if (RUNTIME_STATE_KEYS.has(keyPath)) return true;
return DYNAMIC_KEY_PATTERNS.some((p) => p.test(keyPath));
}

View File

@@ -55,8 +55,9 @@ describe('VALID_CONFIG_KEYS correctness', () => {
});
test('#3162: workflow._auto_chain_active must be accepted by isValidConfigKey (written by workflows)', () => {
assert.ok(
assert.strictEqual(
isValidConfigKey('workflow._auto_chain_active'),
true,
'workflow._auto_chain_active is written by plan-phase, execute-phase, discuss-phase, transition workflows via config-set'
);
});

View File

@@ -23,18 +23,22 @@ const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const { VALID_CONFIG_KEYS: CJS_KEYS, DYNAMIC_KEY_PATTERNS: CJS_PATTERNS } =
const {
VALID_CONFIG_KEYS: CJS_KEYS,
RUNTIME_STATE_KEYS: CJS_RUNTIME_KEYS,
DYNAMIC_KEY_PATTERNS: CJS_PATTERNS,
} =
require('../get-shit-done/bin/lib/config-schema.cjs');
const SDK_SCHEMA_PATH = path.join(ROOT, 'sdk', 'src', 'query', 'config-schema.ts');
const SDK_SRC = fs.readFileSync(SDK_SCHEMA_PATH, 'utf8');
function extractSdkKeys(src) {
const start = src.indexOf('VALID_CONFIG_KEYS');
assert.ok(start > -1, 'SDK config-schema.ts must export VALID_CONFIG_KEYS');
function extractSdkSet(src, setName) {
const start = src.indexOf(setName);
assert.ok(start > -1, `SDK config-schema.ts must export ${setName}`);
const setOpen = src.indexOf('new Set([', start);
const setClose = src.indexOf('])', setOpen);
assert.ok(setOpen > -1 && setClose > -1, 'VALID_CONFIG_KEYS must be a new Set([...]) literal');
assert.ok(setOpen > -1 && setClose > -1, `${setName} must be a new Set([...]) literal`);
const body = src.slice(setOpen + 'new Set(['.length, setClose);
const keys = new Set();
for (const match of body.matchAll(/'([^']+)'/g)) keys.add(match[1]);
@@ -52,7 +56,7 @@ function extractSdkPatternSources(src) {
}
test('#2653 — SDK VALID_CONFIG_KEYS matches CJS VALID_CONFIG_KEYS', () => {
const sdkKeys = extractSdkKeys(SDK_SRC);
const sdkKeys = extractSdkSet(SDK_SRC, 'VALID_CONFIG_KEYS');
const missingInSdk = [...CJS_KEYS].filter((k) => !sdkKeys.has(k));
const extraInSdk = [...sdkKeys].filter((k) => !CJS_KEYS.has(k));
assert.deepStrictEqual(
@@ -69,6 +73,24 @@ test('#2653 — SDK VALID_CONFIG_KEYS matches CJS VALID_CONFIG_KEYS', () => {
);
});
test('#3162 — SDK RUNTIME_STATE_KEYS matches CJS RUNTIME_STATE_KEYS', () => {
const sdkRuntimeKeys = extractSdkSet(SDK_SRC, 'RUNTIME_STATE_KEYS');
const missingInSdk = [...CJS_RUNTIME_KEYS].filter((k) => !sdkRuntimeKeys.has(k));
const extraInSdk = [...sdkRuntimeKeys].filter((k) => !CJS_RUNTIME_KEYS.has(k));
assert.deepStrictEqual(
missingInSdk,
[],
'CJS runtime-state keys missing from sdk/src/query/config-schema.ts:\n' +
missingInSdk.map((k) => ' ' + k).join('\n'),
);
assert.deepStrictEqual(
extraInSdk,
[],
'SDK runtime-state keys missing from get-shit-done/bin/lib/config-schema.cjs:\n' +
extraInSdk.map((k) => ' ' + k).join('\n'),
);
});
test('#2653 — SDK DYNAMIC_KEY_PATTERNS sources match CJS regex .source', () => {
const sdkSources = new Set(extractSdkPatternSources(SDK_SRC));
const cjsSources = CJS_PATTERNS.map((p) => {