import type { SettingsManager } from "@earendil-works/pi-coding-agent"; import { Minimatch } from "minimatch"; import { type Static, Type } from "typebox"; import { Value } from "typebox/value"; import { DEFAULT_MAX_SUBAGENT_DEPTH, DEFAULT_TOOLSETS, THINKING_LEVELS, } from "./minimal-subagents-capabilities.js"; import type { MinimalSubagentsToolsets } from "./minimal-subagents-types.js"; const MODEL_ROLE_NAME_MAX_LENGTH = 64; const MODEL_ROLE_HINT_MAX_LENGTH = 500; const SettingsDocumentSchema = Type.Object({ minimalSubagents: Type.Optional(Type.Unknown()), }); const MinimalSubagentsSettingsSchema = Type.Object({ enabled: Type.Optional(Type.Unknown()), maxSubagentDepth: Type.Optional(Type.Unknown()), modelRoles: Type.Optional(Type.Unknown()), baseToolset: Type.Optional(Type.Unknown()), readToolset: Type.Optional(Type.Unknown()), modifyToolset: Type.Optional(Type.Unknown()), }); const ModelRoleObjectSchema = Type.Object({ model: Type.Optional(Type.Unknown()), hint: Type.Optional(Type.Unknown()), }); const EnabledSettingSchema = Type.Boolean(); const ToolsetSchema = Type.Array(Type.Unknown()); const ToolPatternSchema = Type.String({ minLength: 1 }); const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }); const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]); const ShorthandModelRoleSchema = Type.String(); const ExpandedModelRoleSchema = Type.Object( { model: Type.String(), hint: Type.Optional(Type.String()), }, { additionalProperties: false }, ); const ModelRoleEntriesSchema = Type.Record(Type.String(), Type.Unknown()); const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]); type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number]; /** Describes one user-authored advisory model role shown to subagent callers. */ export interface MinimalSubagentsModelRole { name: string; model: string; thinkingLevel?: ModelRoleThinkingLevel; hint?: string; } /** Identifies the authored settings layer that determines Subagent Access. */ export type SubagentAccessSettingsSource = "project" | "global" | "default"; /** Contains effective and authored Subagent Access settings for one Root Agent session. */ export interface ResolvedSubagentAccessSettings { enabled: boolean; source: SubagentAccessSettingsSource; globalEnabled: boolean | undefined; projectEnabled: boolean | undefined; } /** Contains the validated minimal subagents settings used by one extension session. */ export interface ResolvedMinimalSubagentsConfig { maxSubagentDepth: number; subagentAccess: ResolvedSubagentAccessSettings; modelRoles: MinimalSubagentsModelRole[]; toolsets: MinimalSubagentsToolsets; warnings: string[]; } interface MinimalSubagentsSettingsDocument { minimalSubagents?: unknown; } interface MinimalSubagentsConfigInput { globalSettings: MinimalSubagentsSettingsDocumentInput; projectSettings: MinimalSubagentsSettingsDocumentInput; projectTrusted?: boolean; eligibleModelIds: readonly string[]; } type PiSettingsDocument = ReturnType; type MinimalSubagentsSettingsDocumentInput = PiSettingsDocument | MinimalSubagentsSettingsDocument; interface MinimalSubagentsSettingsReader { getGlobalSettings(): MinimalSubagentsSettingsDocumentInput; getProjectSettings(): MinimalSubagentsSettingsDocumentInput; isProjectTrusted?(): boolean; } type SettingsScope = "global" | "project"; interface ScopedSettingValue { scope: SettingsScope; value: ModelRoleWireValue; } interface ParsedMinimalSubagentsSettings extends Partial { enabled?: boolean; maxSubagentDepth?: MaxSubagentDepthWireValue; modelRoles?: ModelRolesWireValue; } type MaxSubagentDepthWireValue = | { kind: "depth"; value: number } | { kind: "reset" } | { kind: "invalid" }; type ModelRoleWireValue = | { kind: "delete" } | { kind: "shorthand"; model: string } | { kind: "expanded"; fields: Static } | { kind: "malformed-expanded"; fields: Static } | { kind: "invalid" }; type ModelRolesWireValue = | { kind: "reset" } | { kind: "entries"; entries: ReadonlyMap } | { kind: "invalid" }; // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Authored settings remain unparsed until the depth schema below validates their value. function parseMaxSubagentDepthWireValue(value: unknown): MaxSubagentDepthWireValue { if (!Value.Check(MaxSubagentDepthSettingSchema, value)) return { kind: "invalid" }; return value === null ? { kind: "reset" } : { kind: "depth", value }; } // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Role entries may contain arbitrary settings data; schemas classify them before model/hint validation. function parseModelRoleWireValue(value: unknown): ModelRoleWireValue { if (value === null) return { kind: "delete" }; if (Value.Check(ShorthandModelRoleSchema, value)) return { kind: "shorthand", model: value }; if (Value.Check(ModelRoleObjectSchema, value)) { return Value.Check(ExpandedModelRoleSchema, value) ? { kind: "expanded", fields: value } : { kind: "malformed-expanded", fields: value }; } return { kind: "invalid" }; } function isExpandedModelRoleWireValue( value: ModelRoleWireValue, ): value is Extract { return value.kind === "expanded" || value.kind === "malformed-expanded"; } // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Validate the authored role collection before parsing each untrusted entry. function parseModelRolesWireValue(value: unknown): ModelRolesWireValue { if (!Value.Check(ModelRolesSettingSchema, value)) return { kind: "invalid" }; if (value === null) return { kind: "reset" }; return { kind: "entries", entries: new Map( Object.entries(value).map(([name, roleValue]) => [name, parseModelRoleWireValue(roleValue)]), ), }; } function readMinimalSubagentsSettings( settings: MinimalSubagentsSettingsDocumentInput, scope: SettingsScope, warnings: string[], ): ParsedMinimalSubagentsSettings { if (!Value.Check(SettingsDocumentSchema, settings)) return {}; const minimalSubagents = settings.minimalSubagents; if (minimalSubagents === undefined) return {}; if (Value.Check(MinimalSubagentsSettingsSchema, minimalSubagents)) { const parsed: ParsedMinimalSubagentsSettings = {}; if (minimalSubagents.enabled !== undefined) { if (Value.Check(EnabledSettingSchema, minimalSubagents.enabled)) { parsed.enabled = minimalSubagents.enabled; } else { warnings.push(`${scope} minimalSubagents.enabled: expected a boolean`); } } if (minimalSubagents.maxSubagentDepth !== undefined) { parsed.maxSubagentDepth = parseMaxSubagentDepthWireValue(minimalSubagents.maxSubagentDepth); } if (minimalSubagents.modelRoles !== undefined) { parsed.modelRoles = parseModelRolesWireValue(minimalSubagents.modelRoles); } for (const key of ["baseToolset", "readToolset", "modifyToolset"] as const) { const value = minimalSubagents[key]; if (value === undefined) continue; const path = `${scope} minimalSubagents.${key}`; if (!Value.Check(ToolsetSchema, value)) { warnings.push(`${path}: expected an array of patterns`); continue; } const patterns: string[] = []; for (const [index, pattern] of value.entries()) { if (!Value.Check(ToolPatternSchema, pattern)) { warnings.push(`${path}[${index}]: expected a non-empty string`); continue; } try { if (new Minimatch(pattern).makeRe() !== false) { patterns.push(pattern); continue; } } catch { // Invalid minimatch patterns are nonblocking configuration warnings. } warnings.push(`${path}[${index}]: invalid minimatch pattern`); } parsed[key] = patterns; } return parsed; } warnings.push(`${scope} minimalSubagents: expected an object`); return {}; } function mergeModelRoleEntries( globalValue: ModelRolesWireValue | undefined, projectValue: ModelRolesWireValue | undefined, warnings: string[], ): Map { const entries = new Map(); if (globalValue?.kind === "entries") { for (const [name, value] of globalValue.entries) { entries.set(name, { scope: "global", value }); } } else if (globalValue?.kind === "invalid") { warnings.push("global minimalSubagents.modelRoles: expected an object or null"); } if (projectValue?.kind === "reset") return new Map(); if (projectValue === undefined) return entries; if (projectValue.kind === "invalid") { warnings.push("project minimalSubagents.modelRoles: expected an object or null"); return entries; } if (projectValue.kind !== "entries") return entries; for (const [name, value] of projectValue.entries) { if (value.kind === "delete") { entries.delete(name); continue; } const inherited = entries.get(name)?.value; const mergedValue = inherited !== undefined && isExpandedModelRoleWireValue(inherited) && isExpandedModelRoleWireValue(value) ? parseModelRoleWireValue({ ...inherited.fields, ...value.fields }) : value; entries.set(name, { scope: "project", value: mergedValue }); } return entries; } interface ResolvedModelRoleReference { model: string; thinkingLevel?: ModelRoleThinkingLevel; } function resolveThinkingLevelSuffix(suffix: string): ModelRoleThinkingLevel | undefined { return THINKING_LEVELS.find((thinkingLevel) => thinkingLevel === suffix); } function resolveModelRoleReference( model: string, eligibleModels: ReadonlySet, path: string, warnings: string[], ): ResolvedModelRoleReference | undefined { if (eligibleModels.has(model)) return { model }; const separatorIndex = model.lastIndexOf(":"); if (separatorIndex >= 0) { const prefix = model.slice(0, separatorIndex); const suffix = model.slice(separatorIndex + 1); if (eligibleModels.has(prefix)) { const thinkingLevel = resolveThinkingLevelSuffix(suffix); if (thinkingLevel !== undefined) return { model: prefix, thinkingLevel }; warnings.push(`${path}: unknown thinking level suffix: ${suffix}`); return undefined; } } warnings.push(`${path}: model is not eligible: ${model}`); return undefined; } function parseModelRoles( entries: ReadonlyMap, eligibleModelIds: readonly string[], warnings: string[], ): MinimalSubagentsModelRole[] { const eligibleModels = new Set(eligibleModelIds); const roles: MinimalSubagentsModelRole[] = []; for (const [name, entry] of entries) { const path = `${entry.scope} minimalSubagents.modelRoles.${name}`; if ( name.length === 0 || name !== name.trim() || /[\r\n]/.test(name) || name.length > MODEL_ROLE_NAME_MAX_LENGTH ) { warnings.push(`${path}: role name must be trimmed single-line text up to 64 characters`); continue; } const value = entry.value; const expandedRoleObject = isExpandedModelRoleWireValue(value) ? value.fields : undefined; if (expandedRoleObject !== undefined) { const invalidFields = Object.keys(expandedRoleObject).filter( (key) => key !== "model" && key !== "hint", ); if (invalidFields.length > 0) { warnings.push(`${path}: unknown field: ${invalidFields.join(", ")}`); continue; } } else if (value.kind !== "shorthand") { warnings.push(`${path}: expected a model string or expanded role object`); continue; } const modelValue = expandedRoleObject === undefined && value.kind === "shorthand" ? value.model : expandedRoleObject?.model; if ( !Value.Check(ShorthandModelRoleSchema, modelValue) || modelValue.length === 0 || modelValue !== modelValue.trim() ) { warnings.push(`${path}: model must be a non-empty trimmed string`); continue; } const resolvedModel = resolveModelRoleReference(modelValue, eligibleModels, path, warnings); if (resolvedModel === undefined) continue; const hintValue = expandedRoleObject?.hint; if ( hintValue !== undefined && (!Value.Check(ShorthandModelRoleSchema, hintValue) || hintValue.length === 0 || hintValue !== hintValue.trim() || /[\r\n]/.test(hintValue) || hintValue.length > MODEL_ROLE_HINT_MAX_LENGTH) ) { warnings.push(`${path}.hint: expected trimmed single-line text up to 500 characters`); continue; } const role: MinimalSubagentsModelRole = { name, model: resolvedModel.model, }; if (resolvedModel.thinkingLevel !== undefined) { role.thinkingLevel = resolvedModel.thinkingLevel; } if (hintValue !== undefined && Value.Check(ShorthandModelRoleSchema, hintValue)) { role.hint = hintValue; } roles.push(role); } return roles; } function resolveSubagentAccessSettings( globalEnabled: boolean | undefined, projectEnabled: boolean | undefined, ): ResolvedSubagentAccessSettings { const source = projectEnabled !== undefined ? "project" : globalEnabled !== undefined ? "global" : "default"; return { enabled: projectEnabled ?? globalEnabled ?? true, source, globalEnabled, projectEnabled, }; } function resolveMaxSubagentDepth( globalValue: MaxSubagentDepthWireValue | undefined, projectValue: MaxSubagentDepthWireValue | undefined, warnings: string[], ): number { let resolvedDepth = DEFAULT_MAX_SUBAGENT_DEPTH; if (globalValue?.kind === "depth") { resolvedDepth = globalValue.value; } else if (globalValue?.kind === "invalid") { warnings.push( "global minimalSubagents.maxSubagentDepth: expected a positive safe integer or null", ); } if (projectValue === undefined) return resolvedDepth; if (projectValue.kind === "reset") return DEFAULT_MAX_SUBAGENT_DEPTH; if (projectValue.kind === "depth") return projectValue.value; warnings.push( "project minimalSubagents.maxSubagentDepth: expected a positive safe integer or null", ); return resolvedDepth; } /** Resolve trusted global and project settings into validated subagent guidance and limits. */ export function resolveMinimalSubagentsConfig( input: MinimalSubagentsConfigInput, ): ResolvedMinimalSubagentsConfig { const warnings: string[] = []; const globalConfig = readMinimalSubagentsSettings(input.globalSettings, "global", warnings); const projectConfig = input.projectTrusted === false ? {} : readMinimalSubagentsSettings(input.projectSettings, "project", warnings); const maxSubagentDepth = resolveMaxSubagentDepth( globalConfig.maxSubagentDepth, projectConfig.maxSubagentDepth, warnings, ); const modelRoleEntries = mergeModelRoleEntries( globalConfig.modelRoles, projectConfig.modelRoles, warnings, ); return { maxSubagentDepth, subagentAccess: resolveSubagentAccessSettings(globalConfig.enabled, projectConfig.enabled), modelRoles: parseModelRoles(modelRoleEntries, input.eligibleModelIds, warnings), toolsets: { baseToolset: projectConfig.baseToolset ?? globalConfig.baseToolset ?? [...DEFAULT_TOOLSETS.baseToolset], readToolset: projectConfig.readToolset ?? globalConfig.readToolset ?? [...DEFAULT_TOOLSETS.readToolset], modifyToolset: projectConfig.modifyToolset ?? globalConfig.modifyToolset ?? [...DEFAULT_TOOLSETS.modifyToolset], }, warnings, }; } /** Resolve model roles and depth from Pi's trust-aware global and project settings layers. */ export function resolveMinimalSubagentsSettings( settings: MinimalSubagentsSettingsReader, eligibleModelIds: readonly string[], ): ResolvedMinimalSubagentsConfig { return resolveMinimalSubagentsConfig({ globalSettings: settings.getGlobalSettings(), projectSettings: settings.getProjectSettings(), projectTrusted: settings.isProjectTrusted?.() ?? true, eligibleModelIds, }); }