{"version":3,"file":"model-scope.d.ts","sourceRoot":"","sources":["../../../../src/runs/shared/model-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,WAAW,gBAAgB;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnD,MAAM,WAAW,mBAAmB;IACnC,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC1B;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3E;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,EAAE,gBAAgB,GAAG,SAAS,EACnC,MAAM,EAAE,WAAW,GACjB,mBAAmB,GAAG,SAAS,CAgBjC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,gBAAgB,GAAG,SAAS,CAiD9G","sourcesContent":["/**\n * Optional model-scope enforcement for subagent model resolution.\n *\n * When `subagents.modelScope.enforce` is enabled in settings, a resolved model\n * that does not match any `allow` pattern is rejected. The severity depends on\n * where the model came from: an explicit caller-supplied model (`--model`,\n * tool-call `model`, or a TUI clarify pick) is a hard error, while a model\n * inherited from agent frontmatter / `defaultModel` / the parent session only\n * emits a warning so existing configurations keep working.\n *\n * The decision logic ({@link checkModelScope}) is a pure function of its\n * inputs so it can be unit-tested without touching the filesystem or config.\n */\n\nimport { splitKnownThinkingSuffix } from \"../../shared/model-info.ts\";\n\nexport interface ModelScopeConfig {\n\tenforce?: boolean;\n\t/** Glob-style allow patterns (only `*` is special), matched against `provider/id`. */\n\tallow?: string[];\n}\n\n/** Where a resolved model originated, deciding enforcement severity. */\nexport type ModelSource = \"explicit\" | \"inherited\";\n\nexport interface ModelScopeViolation {\n\t/** Resolved model id (without thinking suffix) that fell outside the scope. */\n\tmodel: string;\n\tseverity: \"warn\" | \"error\";\n\tmessage: string;\n\tallowedPatterns: string[];\n}\n\nfunction stripThinkingSuffix(model: string): string {\n\treturn splitKnownThinkingSuffix(model).baseModel;\n}\n\n/** Escape RegExp specials except `*`, then turn `*` into `.*`. */\nfunction globToRegExp(pattern: string): RegExp {\n\tconst escaped = pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\");\n\treturn new RegExp(`^${escaped}$`, \"i\");\n}\n\n/**\n * Test whether a resolved model matches a single allow pattern. Both sides are\n * compared case-insensitively against the full `provider/id` (thinking suffix\n * stripped from the model).\n */\nexport function matchesScopePattern(model: string, pattern: string): boolean {\n\treturn globToRegExp(pattern).test(stripThinkingSuffix(model));\n}\n\n/**\n * Pure scope decision. Returns a {@link ModelScopeViolation} when the model is\n * out of scope and enforcement is on, otherwise `undefined`. Enforcement with\n * no `allow` list is a no-op (the settings parser rejects that combination, but\n * this stays defensive for callers that build configs programmatically).\n */\nexport function checkModelScope(\n\tmodel: string | undefined,\n\tscope: ModelScopeConfig | undefined,\n\tsource: ModelSource,\n): ModelScopeViolation | undefined {\n\tif (!model || !scope?.enforce) return undefined;\n\tconst allow = scope.allow;\n\tif (!allow || allow.length === 0) return undefined;\n\tif (allow.some((pattern) => matchesScopePattern(model, pattern))) return undefined;\n\n\tconst baseModel = stripThinkingSuffix(model);\n\tconst severity: ModelScopeViolation[\"severity\"] = source === \"explicit\" ? \"error\" : \"warn\";\n\treturn {\n\t\tmodel: baseModel,\n\t\tseverity,\n\t\tallowedPatterns: allow,\n\t\tmessage:\n\t\t\t`Model '${baseModel}' is outside the configured subagent model scope. ` +\n\t\t\t`Allowed patterns: ${allow.join(\", \")}.`,\n\t};\n}\n\n/**\n * Validate and normalize a raw `subagents.modelScope` value from settings.\n * Throws a descriptive error for malformed configs (matching the surrounding\n * settings-parsing style). Returns `undefined` when the field is absent.\n */\nexport function parseModelScopeConfig(value: unknown, meta: { filePath: string }): ModelScopeConfig | undefined {\n\tif (value === undefined) return undefined;\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope'; expected an object.`);\n\t}\n\n\tconst input = value as Record<string, unknown>;\n\tconst config: ModelScopeConfig = {};\n\n\tif (\"enforce\" in input) {\n\t\tif (typeof input.enforce !== \"boolean\") {\n\t\t\tthrow new Error(\n\t\t\t\t`Subagent settings in '${meta.filePath}' have invalid 'modelScope.enforce'; expected a boolean.`,\n\t\t\t);\n\t\t}\n\t\tconfig.enforce = input.enforce;\n\t}\n\n\tif (\"allow\" in input) {\n\t\tif (!Array.isArray(input.allow)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`,\n\t\t\t);\n\t\t}\n\t\tconst allow: string[] = [];\n\t\tfor (const entry of input.allow) {\n\t\t\tif (typeof entry !== \"string\") {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst trimmed = entry.trim();\n\t\t\tif (trimmed) allow.push(trimmed);\n\t\t}\n\t\tif (allow.length === 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected a non-empty array of patterns.`,\n\t\t\t);\n\t\t}\n\t\tconfig.allow = allow;\n\t}\n\n\tif (config.enforce === true && (!config.allow || config.allow.length === 0)) {\n\t\tthrow new Error(\n\t\t\t`Subagent settings in '${meta.filePath}' set modelScope.enforce without a non-empty 'allow' list; supply allowed model patterns or disable enforcement.`,\n\t\t);\n\t}\n\n\treturn Object.keys(config).length > 0 ? config : undefined;\n}\n"]}