Skip to content

Commit 7e79cef

Browse files
Merge pull request #279 from corbitsdev/cl-4843-fix-silent-malformed-data-swallowing-in-the-project-trust
Refactor train: trust store, settings load, shell tokenize, session assembly, sub-agent split
2 parents 02de734 + 5e9ea77 commit 7e79cef

20 files changed

Lines changed: 3298 additions & 2412 deletions

src/agent/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type { ProviderCatalogEntry } from "../config/index.js";
3838
import type { AgentProfile } from "./profiles.js";
3939
import {
4040
createTaskTool,
41+
runSubAgent,
4142
type SubAgentProvider,
4243
type SubAgentSessionStore,
4344
} from "../subagent/index.js";
@@ -215,6 +216,7 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise<AgentT
215216
provider: args.subAgent.provider,
216217
permissionGate,
217218
inheritMcpTools: () => inheritedMcpTools,
219+
run: runSubAgent,
218220
...(shellTimeout !== undefined ? { shellTimeout } : {}),
219221
...(shellEnv !== undefined ? { shellEnv } : {}),
220222
...(extraToolPlugins.length > 0 ? { extraToolPlugins } : {}),

src/config/settings.ts

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,67 @@ export function isLocalSettings(value: unknown): value is LocalSettings {
494494
return true;
495495
}
496496

497+
// Drop keys whose value is undefined so JSON omit + optional Settings fields stay
498+
// aligned. Value transforms (normalize, clamp, enum checks) happen before this —
499+
// the helper only filters undefined, it does not validate.
500+
type DefinedFields<T> = {
501+
[K in keyof T as undefined extends T[K] ? (T[K] extends undefined ? never : K) : K]: Exclude<
502+
T[K],
503+
undefined
504+
>;
505+
};
506+
507+
function pickDefined<T extends Record<string, unknown>>(fields: T): DefinedFields<T> {
508+
const out: Record<string, unknown> = {};
509+
for (const [key, value] of Object.entries(fields)) {
510+
if (value !== undefined) out[key] = value;
511+
}
512+
return out as DefinedFields<T>;
513+
}
514+
515+
// Every optional Settings key must appear here so a new type field without a
516+
// load-path assignment fails at compile time instead of silently dropping on
517+
// the next load/save cycle.
518+
type OptionalSettingsFields = {
519+
[K in Exclude<keyof Settings, "providers">]: Settings[K] | undefined;
520+
};
521+
522+
type OptionalLocalSettingsFields = {
523+
[K in keyof LocalSettings]: LocalSettings[K] | undefined;
524+
};
525+
526+
/** Optional global settings keys the load path is required to consider. */
527+
export const GLOBAL_SETTINGS_OPTIONAL_KEYS = [
528+
"defaultProvider",
529+
"mcpServers",
530+
"tiers",
531+
"workflowProfiles",
532+
"plugins",
533+
"pluginPaths",
534+
"discoverClaudePlugins",
535+
"web",
536+
"hiddenCommands",
537+
"onboarded",
538+
"compactionMode",
539+
"maxConcurrentSubAgents",
540+
"subagentMaxTurns",
541+
"sessionMode",
542+
"agentModelFallback",
543+
"shell",
544+
"tools",
545+
"telemetry",
546+
] as const satisfies readonly (keyof OptionalSettingsFields)[];
547+
548+
/** Optional local settings keys the load path is required to consider. */
549+
export const LOCAL_SETTINGS_OPTIONAL_KEYS = [
550+
"provider",
551+
"model",
552+
"reasoningEffort",
553+
"mcpServers",
554+
"sessionMode",
555+
"env",
556+
] as const satisfies readonly (keyof OptionalLocalSettingsFields)[];
557+
497558
export async function loadSettings(path: string): Promise<Settings | null> {
498559
let raw: string;
499560
try {
@@ -523,37 +584,42 @@ export async function loadSettings(path: string): Promise<Settings | null> {
523584
`settings: "workflowPlugins"/"agentPlugins" are no longer supported and will be dropped. Install those plugins under .corbits/plugins/ (or via /plugins "add by path") and enable them in /plugins.\n`,
524585
);
525586
}
587+
// Transforms (normalize/clamp/enum) first; pickDefined only drops undefined.
588+
const optional: OptionalSettingsFields = {
589+
defaultProvider: s.defaultProvider as string | undefined,
590+
mcpServers: s.mcpServers !== undefined ? normalizeMcpServers(s.mcpServers) : undefined,
591+
tiers: s.tiers as Settings["tiers"] | undefined,
592+
workflowProfiles: s.workflowProfiles as Settings["workflowProfiles"] | undefined,
593+
plugins: s.plugins as Settings["plugins"] | undefined,
594+
pluginPaths: s.pluginPaths as string[] | undefined,
595+
discoverClaudePlugins: s.discoverClaudePlugins === true ? true : undefined,
596+
web: s.web as string | undefined,
597+
hiddenCommands: s.hiddenCommands as string[] | undefined,
598+
onboarded: s.onboarded !== undefined ? Boolean(s.onboarded) : undefined,
599+
compactionMode:
600+
s.compactionMode === "llm" || s.compactionMode === "pruning" ? s.compactionMode : undefined,
601+
maxConcurrentSubAgents:
602+
s.maxConcurrentSubAgents !== undefined
603+
? clampMaxConcurrentSubAgents(s.maxConcurrentSubAgents as number)
604+
: undefined,
605+
subagentMaxTurns:
606+
s.subagentMaxTurns !== undefined
607+
? clampSubAgentMaxTurns(s.subagentMaxTurns as number)
608+
: undefined,
609+
sessionMode:
610+
s.sessionMode === "single" || s.sessionMode === "orchestrator" ? s.sessionMode : undefined,
611+
agentModelFallback:
612+
s.agentModelFallback === "active" || s.agentModelFallback === "none"
613+
? s.agentModelFallback
614+
: undefined,
615+
shell: s.shell as Settings["shell"] | undefined,
616+
tools: s.tools as Settings["tools"] | undefined,
617+
telemetry: s.telemetry as Settings["telemetry"] | undefined,
618+
};
526619
return {
527620
providers: s.providers as Settings["providers"],
528-
...(s.defaultProvider !== undefined ? { defaultProvider: s.defaultProvider as string } : {}),
529-
...(s.mcpServers !== undefined ? { mcpServers: normalizeMcpServers(s.mcpServers) } : {}),
530-
...(s.tiers !== undefined ? { tiers: s.tiers as Settings["tiers"] } : {}),
531-
...(s.workflowProfiles !== undefined ? { workflowProfiles: s.workflowProfiles as Settings["workflowProfiles"] } : {}),
532-
...(s.plugins !== undefined ? { plugins: s.plugins as Settings["plugins"] } : {}),
533-
...(s.pluginPaths !== undefined ? { pluginPaths: s.pluginPaths as string[] } : {}),
534-
...(s.discoverClaudePlugins === true ? { discoverClaudePlugins: true } : {}),
535-
...(s.web !== undefined ? { web: s.web as string } : {}),
536-
...(s.hiddenCommands !== undefined ? { hiddenCommands: s.hiddenCommands as string[] } : {}),
537-
...(s.onboarded !== undefined ? { onboarded: Boolean(s.onboarded) } : {}),
538-
...(s.compactionMode === "llm" || s.compactionMode === "pruning"
539-
? { compactionMode: s.compactionMode }
540-
: {}),
541-
...(s.maxConcurrentSubAgents !== undefined
542-
? { maxConcurrentSubAgents: clampMaxConcurrentSubAgents(s.maxConcurrentSubAgents as number) }
543-
: {}),
544-
...(s.subagentMaxTurns !== undefined
545-
? { subagentMaxTurns: clampSubAgentMaxTurns(s.subagentMaxTurns as number) }
546-
: {}),
547-
...(s.sessionMode === "single" || s.sessionMode === "orchestrator"
548-
? { sessionMode: s.sessionMode }
549-
: {}),
550-
...(s.agentModelFallback === "active" || s.agentModelFallback === "none"
551-
? { agentModelFallback: s.agentModelFallback }
552-
: {}),
553-
...(s.shell !== undefined ? { shell: s.shell as Settings["shell"] } : {}),
554-
...(s.tools !== undefined ? { tools: s.tools as Settings["tools"] } : {}),
555-
...(s.telemetry !== undefined ? { telemetry: s.telemetry as Settings["telemetry"] } : {}),
556-
} as Settings;
621+
...pickDefined(optional),
622+
};
557623
}
558624

559625
export async function loadLocalSettings(path: string): Promise<LocalSettings | null> {
@@ -572,19 +638,20 @@ export async function loadLocalSettings(path: string): Promise<LocalSettings | n
572638
}
573639
if (!isLocalSettings(parsed)) {
574640
throw new Error(
575-
`Invalid local settings in ${path}: only "provider", "model", "reasoningEffort", "mcpServers", and "sessionMode" are allowed (no credentials).`,
641+
`Invalid local settings in ${path}: only "provider", "model", "reasoningEffort", "mcpServers", "sessionMode", and "env" are allowed (no credentials).`,
576642
);
577643
}
578644
const s = parsed as Record<string, unknown>;
579-
return {
580-
...(s.provider !== undefined ? { provider: s.provider as string } : {}),
581-
...(s.model !== undefined ? { model: s.model as string } : {}),
582-
...(s.reasoningEffort !== undefined ? { reasoningEffort: s.reasoningEffort as ReasoningEffort } : {}),
583-
...(s.mcpServers !== undefined ? { mcpServers: normalizeMcpServers(s.mcpServers) } : {}),
584-
...(s.sessionMode === "single" || s.sessionMode === "orchestrator"
585-
? { sessionMode: s.sessionMode }
586-
: {}),
587-
} as LocalSettings;
645+
const optional: OptionalLocalSettingsFields = {
646+
provider: s.provider as string | undefined,
647+
model: s.model as string | undefined,
648+
reasoningEffort: s.reasoningEffort as ReasoningEffort | undefined,
649+
mcpServers: s.mcpServers !== undefined ? normalizeMcpServers(s.mcpServers) : undefined,
650+
sessionMode:
651+
s.sessionMode === "single" || s.sessionMode === "orchestrator" ? s.sessionMode : undefined,
652+
env: s.env as Record<string, string> | undefined,
653+
};
654+
return pickDefined(optional);
588655
}
589656

590657
// Resolve the base for a read-modify-write of the global settings file.

0 commit comments

Comments
 (0)