/** * Override loading (D-006): code defaults plus a validated override file; * the override wins, and a malformed file - structurally OR semantically - * throws with the file path and offending harness in the message. A silent * fallback to defaults, or a garbage value flowing onward, would ship an * override bug invisibly. Pure over TEXT: the caller reads the file and * passes its content; nothing here touches the filesystem. * * Merge semantics: recursive to full depth for plain-object sections (a * partial nested override keeps its sibling keys); arrays replace * wholesale; every leaf is type-checked against the code default's shape, * closed string vocabularies are enforced, and any path whose default * carries a RegExp cannot be overridden from JSON. */ import { antigravityCli } from "./antigravity.js"; import { claudeCode } from "./claude-code.js"; import { codexCli } from "./codex.js"; import { cursorCli } from "./cursor.js"; import { HARNESS_NAMES, type HarnessDescriptor, type HarnessName, RESUME_ON_MISSING, SESSION_INPUT_KINDS, } from "./descriptor.js"; import { compileMatcher, MAX_MATCHERS_PER_KIND } from "./matchers.js"; import { museCode } from "./muse.js"; import { piCli } from "./pi.js"; import { popeyeCli } from "./popeye.js"; export type DescriptorSet = Partial>; const SHARED_DESCRIPTORS: DescriptorSet = { claude: claudeCode, codex: codexCli, pi: piCli, muse: museCode, // RFC-05: the cursor entry is also what makes `hcn ls` list it. cursor: cursorCli, antigravity: antigravityCli, popeye: popeyeCli, }; export const defaultDescriptors = (): DescriptorSet => SHARED_DESCRIPTORS; /** Tracks which descriptors were produced by an override file and what their * matcher counts are, so the execution layer can emit `matcherOverrides` on * the spawn boundary event without re-deriving it. */ export const matcherOverridesOf = new WeakMap< HarnessDescriptor, { limit: number; auth: number; trust: number } >(); export class OverrideRefusalError extends Error { constructor( readonly path: string, detail: string, readonly harness?: string, ) { super( `invalid harness override file ${path}: ${harness === undefined ? "" : `harness ${JSON.stringify(harness)}: `}${detail}`, ); this.name = "OverrideRefusalError"; } } const isPlain = (v: unknown): v is Record => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof RegExp); const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]); const hasRegExpDeep = (value: unknown): boolean => { if (value instanceof RegExp) return true; if (Array.isArray(value)) return value.some(hasRegExpDeep); if (isPlain(value)) return Object.values(value).some(hasRegExpDeep); return false; }; /** Closed string vocabularies by descriptor path - a typo like "Verbatim" * must refuse loudly, not collapse every store path into one directory. */ const enumAt = (keyPath: readonly string[]): readonly string[] | null => { if (keyPath[0] === "capabilities" && keyPath[1] === "streamingByMode") { return ["token", "message", "none"]; } if (keyPath[keyPath.length - 1] === "argvPlacement") { return ["before-prompt", "after-prompt"]; } switch (keyPath.join(".")) { case "stdin": return ["inherit", "close-required"]; case "store.cwdSlug": return ["dash-separators", "pi-dash-wrapped", "verbatim", "md5-hex"]; case "resume.style": return ["flag", "positional"]; case "resume.onMissing": return RESUME_ON_MISSING; case "resumeLast.flagPlacement": return ["before-extra-flags", "after-extra-flags"]; case "identity.authority": return ["caller-assigned", "harness-minted"]; case "output.floor": return ["token", "message", "none"]; case "launch.promptStyle": return ["positional"]; case "sessionMode.input.kind": return SESSION_INPUT_KINDS; default: return null; } }; const mergeValue = ( base: unknown, override: unknown, keyPath: readonly string[], refuse: (detail: string) => never, ): unknown => { const at = keyPath.length === 0 ? "descriptor" : `section ${JSON.stringify(keyPath.join("."))}`; if (base instanceof RegExp) { refuse(`${at} carries a regular expression and cannot be overridden from JSON`); } if (base === null) { if (override === null) return null; refuse(`${at} defaults to null - there is no shape to validate an override against`); } if (isPlain(base)) { if (!isPlain(override)) refuse(`${at} must be an object`); const merged: Record = { ...base }; for (const [key, value] of Object.entries(override)) { if (FORBIDDEN_KEYS.has(key)) refuse(`${at} key ${JSON.stringify(key)} is not allowed`); if (keyPath.length === 0 && key === "name") { refuse(`the "name" field is the registry key and cannot be overridden`); } if (!Object.hasOwn(base, key)) { refuse(`${at} has no field ${JSON.stringify(key)} to override`); } merged[key] = mergeValue(base[key], value, [...keyPath, key], refuse); } return merged; } if (Array.isArray(base)) { if (hasRegExpDeep(base)) { refuse(`${at} carries regular expressions and cannot be overridden from JSON`); } if (!Array.isArray(override)) refuse(`${at} must be an array`); const element = base[0]; if (element === undefined) { // Every empty-default array in the descriptor is a string list. if (override.some((v) => typeof v !== "string")) { refuse(`${at} must be an array of strings`); } return override; } if (isPlain(element)) { // Object elements (output.pins) validate recursively against the // default element's shape - null or wrong-shaped entries must refuse // here, not crash a consumer later. return override.map((v, i) => mergeValue(element, v, [...keyPath, String(i)], refuse)); } if (override.some((v) => typeof v !== typeof element || v === null)) { refuse(`${at} must be an array of ${typeof element}s`); } return override; } if (typeof override !== typeof base) { refuse(`${at} must be a ${typeof base}, got ${typeof override}`); } const allowed = enumAt(keyPath); if (allowed !== null && !allowed.includes(override as string)) { refuse(`${at} must be one of ${allowed.join(", ")}, got ${JSON.stringify(override)}`); } return override; }; const validateMatcherPattern = ( pattern: string, flags: string | undefined, path: string, harness: string, ): void => { // One compiler and one set of bounds, shared with the limits scanner // (RFC-02 change 9); this loader only wraps the refusal. try { compileMatcher(pattern, flags); } catch (e) { throw new OverrideRefusalError(path, (e as Error).message, harness); } }; const validateMatchers = (desc: HarnessDescriptor, path: string, harness: string): void => { const limitMatchers = (desc as unknown as { limitMatchers: readonly unknown[] }).limitMatchers; const authMatchers = (desc as unknown as { authMatchers: readonly unknown[] }).authMatchers; // RFC-05: trust matchers compile under the same bounds as every other // wall, so a crafted override cannot widen trust detection into a // backtracking hazard. Harnesses without the field skip it. const trustMatchers = (desc as unknown as { trustMatchers?: readonly unknown[] }).trustMatchers; if (trustMatchers !== undefined) { if (!Array.isArray(trustMatchers)) { throw new OverrideRefusalError(path, `trust matcher section must be an array`, harness); } if (trustMatchers.length > MAX_MATCHERS_PER_KIND) { throw new OverrideRefusalError( path, `more than ${MAX_MATCHERS_PER_KIND} trust matchers`, harness, ); } for (const m of trustMatchers) { if (!isPlain(m as unknown as Record)) { throw new OverrideRefusalError(path, `trust matcher must be an object`, harness); } const obj = m as Record; if (typeof obj.pattern !== "string") { throw new OverrideRefusalError(path, `trust matcher must have pattern`, harness); } validateMatcherPattern(obj.pattern, obj.flags as string | undefined, path, harness); } } if (Array.isArray(limitMatchers)) { if (limitMatchers.length > MAX_MATCHERS_PER_KIND) { throw new OverrideRefusalError( path, `more than ${MAX_MATCHERS_PER_KIND} limit matchers`, harness, ); } for (const m of limitMatchers) { if (!isPlain(m as unknown as Record)) { throw new OverrideRefusalError(path, `limit matcher must be an object`, harness); } const obj = m as Record; if (typeof obj.pattern !== "string" || typeof obj.code !== "string") { throw new OverrideRefusalError(path, `limit matcher must have pattern and code`, harness); } validateMatcherPattern(obj.pattern, obj.flags as string | undefined, path, harness); } } if (Array.isArray(authMatchers)) { if (authMatchers.length > MAX_MATCHERS_PER_KIND) { throw new OverrideRefusalError( path, `more than ${MAX_MATCHERS_PER_KIND} auth matchers`, harness, ); } for (const m of authMatchers) { if (!isPlain(m as unknown as Record)) { throw new OverrideRefusalError(path, `auth matcher must be an object`, harness); } const obj = m as Record; if (typeof obj.pattern !== "string" || typeof obj.kind !== "string") { throw new OverrideRefusalError(path, `auth matcher must have pattern and kind`, harness); } validateMatcherPattern(obj.pattern, obj.flags as string | undefined, path, harness); } } }; /** Parse an override document and merge it over the code defaults. */ export const parseOverrides = (jsonText: string, path: string): DescriptorSet => { let doc: unknown; try { doc = JSON.parse(jsonText); } catch (cause) { throw new OverrideRefusalError(path, `not valid JSON (${(cause as Error).message})`); } if (!isPlain(doc)) { throw new OverrideRefusalError(path, "top level must be an object keyed by harness name"); } const defaults = defaultDescriptors(); const merged: DescriptorSet = { ...defaults }; for (const [name, sections] of Object.entries(doc)) { const refuse = (detail: string): never => { throw new OverrideRefusalError(path, detail, name); }; if (!HARNESS_NAMES.includes(name as HarnessName)) { refuse(`unknown harness; known: ${HARNESS_NAMES.join(", ")}`); } const base = defaults[name as HarnessName]; if (base === undefined) { refuse("this harness has no code defaults to override yet"); } const next = mergeValue(base, sections, [], refuse) as HarnessDescriptor; if (next.store.template.includes("..")) { refuse('store.template must not contain ".."'); } // Validate matcher compilation at load time, not at first line validateMatchers(next, path, name); // Record matcher counts for the spawn boundary event when this descriptor was overridden const defaultsForHarness = defaults[name as HarnessName] as HarnessDescriptor; const limitChanged = (next.limitMatchers as unknown as ReadonlyArray).length !== (defaultsForHarness.limitMatchers as unknown as ReadonlyArray).length || JSON.stringify(next.limitMatchers) !== JSON.stringify(defaultsForHarness.limitMatchers); const authChanged = (next.authMatchers as unknown as ReadonlyArray).length !== (defaultsForHarness.authMatchers as unknown as ReadonlyArray).length || JSON.stringify(next.authMatchers) !== JSON.stringify(defaultsForHarness.authMatchers); const nextTrust = (next as unknown as { trustMatchers?: readonly unknown[] }).trustMatchers; const baseTrust = (defaultsForHarness as unknown as { trustMatchers?: readonly unknown[] }) .trustMatchers; const trustChanged = (nextTrust === undefined) !== (baseTrust === undefined) || JSON.stringify(nextTrust ?? []) !== JSON.stringify(baseTrust ?? []); if (limitChanged || authChanged || trustChanged) { matcherOverridesOf.set(next, { limit: (next.limitMatchers as unknown as ReadonlyArray).length, auth: (next.authMatchers as unknown as ReadonlyArray).length, trust: nextTrust === undefined ? 0 : nextTrust.length, }); } merged[name as HarnessName] = next; } return merged; };