* test(4032): add failing installed-agent grants contract Cover global and project agent_tools precedence at the real Claude installer seam before adding implementation. * feat(4032): apply configured agent tool grants during staging Resolve selector-level global and project config once per staging call, then append validated grants before runtime conversion. * test(4032): cover host grant and quoted MCP contracts Exercise installed host artifacts and prove ZCode must treat quoted MCP scalars like plain MCP grants. * feat(4032): apply configured agent tool grants across runtimes Move augmentation and scalar identity into the converter seam so every staged artifact preserves host policy. * fix(4032): register agent tool grants in configuration Accept documented agent_tools config without unknown-key warnings.\n\nKeep installer fixtures on the shared temporary-directory helper. * fix(4032): translate configured MCP grants for Kilo Reuse the converter-owned scalar decoder so quoted canonical grants reach Kilo's native permission keys without altering other host policies. * fix(4032): decode YAML-escaped tool grants * fix(4032): emit valid inline agent tool grants * fix(4032): reject invalid trailing-colon grants * test(#4032): cover cross-review remediation gaps * fix(#4032): close cross-runtime grant gaps * test(#4032): expose Kimi global project context * fix(#4032): preserve Kimi project config context * chore(#4032): add release note * test(#4032): expose fork review regressions * fix(#4032): address fork review findings * test(#4032): make byte-stability assertion portable Compare repeat installs at one root so platform-specific path rendering cannot masquerade as an agent_tools behavior change. * chore(#4032): bind changeset to upstream PR 4238 * fix(#4032): address trek-e review findings (2,3,4,5,6,7,8) Fixes fail-closed decode-failure handling in ZCode's mcp__ stripper, a comment-only `tools:` header mis-parse that silently dropped configured grants, and a naive comma-split that could tear a quoted scalar containing a literal comma. Documents Kilo's inherent `{server}_{tool}` MCP-permission-key collision (external, fixed format — not ours to widen) and locks the existing first-seen-wins resolution in with a regression test. Opts kimi/kimi-code out of the ADR-1235 pre-converter path-rewrite step: routing Kimi through that pipeline (needed so project-scoped agent_tools selectors reach it) was short-circuiting Kimi's own neutralizeKimiAgentPrompt, which expects the original ~/.claude/gsd-core text rather than a pre-rewritten Kimi path. Extends the fast-check token pool and per-runtime install coverage with the missing comment/comma/broad-runtime cases the prior review flagged as untested. * docs(#4032): add CONTEXT.md glossary entries for agent_tools resolver + pre-converter step Documents readGsdEffectiveAgentTools (Install Model Override Resolver Module) and the appendAgentTools pre-converter pipeline step (Runtime Artifact Conversion Module), per contributor-standards.md's new-seam glossary requirement (finding 1). * fix(#4032): address agy adversarial review findings An agy (gemini-3.8-flash-high) adversarial pass over the prior review-fix commit found the fixes for findings 3, 4, 6 and 8 had unfixed sibling gaps, plus a genuine new regression and two CONTEXT.md inaccuracies: - ZCode's comment-only `tools: # note` header matched the inline-value branch instead of falling through to the block-list scan, so a following mcp__* item leaked through unstripped — the exact defect finding 4 fixed in appendAgentTools, unfixed in this sibling function. - Reverted capabilities/kimi-code/capability.json's noPathRewrite: true. kimi-code uses the standard 'agents' kind with converter: null (not kimi-agents — confirmed by reading the descriptor, not its prose description), so it never went through the pipeline change finding 5 fixed, and disabling its path rewrite broke every ~/.claude/ embed in its shipped agents instead. - decodeToolScalar never stripped a trailing ` # comment` from a bare (unquoted) scalar, so a comment after a block-list item, or after an appended grant on an inline line, became part of the "tool name" — fixed at the source (one call site fixes every consumer). - appendAgentTools's comment-index scan wasn't quote-aware, so a `#` inside a quoted scalar (`"mcp__server #1"`) was mistaken for a comment start and corrupted the quote. - parseFrontmatterTools (Kimi/Qwen's tool-list reader, downstream of appendAgentTools's own output) had the same naive comma-split and comment-only-header gaps as findings 4 and 6, unpatched. - The all-runtime smoke test's presence assertion was built on a guessed omit-list; empirically only 7 of 17 runtimes keep an arbitrary mcp__ grant recognizable, replaced with a verified allowlist. - CONTEXT.md claimed a `project:<agent>` selector prefix that does not exist (project override is a same-key merge across two config files) and mislabeled stageAgentsForRuntimeWithConverter's module. * fix(#4032): address full-PR review (Opus critical/ponytail + agy) A whole-PR pass (critical-code-reviewer + ponytail-review on Opus, plus a second agy full-source adversarial pass) surfaced defects the earlier finding-scoped passes couldn't reach: - appendAgentTools corrupted a `tools:` line whose ENTIRE value is a leading quoted scalar (`tools: "Read"` -> `tools: "Read", Write`, invalid YAML) — there is no safe line-surgical rewrite here, so it now refuses to touch that shape instead of emitting broken frontmatter. - decodeToolScalar's malformed-trailing-quote check ran BEFORE comment stripping, so a bare tool name with a quote inside its own trailing comment (`Bash # note: "internal"`) was wrongly rejected. Reordered. - findUnquotedCommentIndex (added in the prior remediation commit) was built on a wrong model of YAML: a `#` after whitespace starts a real comment in a plain scalar regardless of nearby quote characters — verified against the actual parser. The one case that DOES need protection (a leading quoted scalar) is now refused outright above, so the quote-tracking scan was dead weight solving a problem that no longer reaches it. Removed; reverted to the plain `[ \t]#` scan. - Kilo has a SEPARATE agent-frontmatter parser (convertClaudeToKiloFrontmatter, distinct from the buildKiloAgentPermissionBlock fixed earlier) with the same comment-only-header and naive-comma-split gaps as findings 4 and 6 — unfixed in both its src/ and bin/install.js copies. Fixed in both, exporting splitToolScalars for bin/install.js to reuse rather than reimplementing it. - Pipeline docstring in stageAgentsForRuntimeWithConverter still listed 5 steps, omitting appendAgentTools (now step 3 of 6). - docs/CONFIGURATION.md didn't state that a --global install still discovers agent_tools from the cwd's .planning/config.json (confirmed intentional and already covered by a dedicated test, not a bug). - Removed install-engine.cts's deps.cwd injection seam: zero callers or tests ever populated it. Two claims from this round were verified and rejected, not fixed: prototype pollution via a `__proto__` selector key (empirically confirmed `Object.prototype` is never touched — only reassigns the resolver's own local object's prototype, with no observable effect), and a `*` grant value crashing YAML parsing as an alias reference (empirically confirmed it parses as plain scalar text, no crash). A pre-existing, unrelated defect (extractFrontmatterField returns null for block-list `tools:` on Copilot/Antigravity/Cursor/Codex/Qwen, affecting two shipped agents today) was filed as a follow-up rather than fixed here — it predates #4032 and isn't caused or worsened by this PR. * fix(#4032): update stale slug-derivation-drift-guard fixture line normalizeKimiSkillName's real closing brace moved from line 616 to 635 as a side effect of this PR's edits to runtime-artifact-conversion.cts; the MAJOR-1 fixture's hardcoded realEndLine had gone stale. * fix(#4032): address CodeRabbit findings on projectDir threading and flow-sequence tools bin/install.js's installAgentsKindStandalone call site omitted the projectDir argument the function already supports, so a global install through this legacy branch silently fell back to the runtime config dir instead of process.cwd() when resolving project-scoped agent_tools grants — inconsistent with the sibling installOpencodeFamilyArtifacts call site, which already threads it correctly. appendAgentTools' leading-quoted-scalar bailout did not cover a YAML flow sequence (`tools: [Bash, Read]`): splitToolScalars tore it apart on the in-sequence commas and appended past its closing bracket, producing invalid frontmatter. Extended the bailout regex to also refuse a value starting with `[`, matching the same "whole node, nothing may follow" reasoning already applied to quoted scalars. --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Test <test@test.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
322 lines
15 KiB
TypeScript
322 lines
15 KiB
TypeScript
/**
|
|
* install-model-override-resolver — install-time per-agent model-override
|
|
* resolution (#2256 / #2794), extracted from the package-root `bin/install.js`
|
|
* (#2875 Part 2 / J8).
|
|
*
|
|
* bin/install.js's inline agent-staging loop duplicated this EXACT precedence
|
|
* chain across two runtime branches (OpenCode ~24 lines, Kilo ~24 lines) —
|
|
* `model_overrides[agent]` > `model_profile_overrides.<runtime>.<tier>` > omit.
|
|
* Extracted here — a `src/*.cts` module compiled into the shipped
|
|
* `gsd-core/bin/lib/` tree, mirroring `install-effort-resolver.cts`'s existing
|
|
* precedent (#2071) — so the descriptor-driven agents pipeline
|
|
* (`convertedAgentsKind` / `stageAgentsForRuntimeWithConverter`) and
|
|
* bin/install.js's own callers resolve through the SAME code. A single source
|
|
* of truth makes the two paths diverge only if this module changes, not
|
|
* silently across two hand-maintained copies (the Generative Fix Divergence
|
|
* class CLAUDE.md's "Known Defects" section warns about).
|
|
*
|
|
* `readGsdGlobalModelOverrides` / `readGsdEffectiveModelOverrides` /
|
|
* `readGsdRuntimeProfileResolver` are impure (config-file reads);
|
|
* `resolveAgentModelOverride` is pure given their pre-resolved outputs.
|
|
*
|
|
* #2875 defect fix: this module is on the `installRuntimeArtifacts` call tree
|
|
* (reached from `runtime-artifact-layout.cts`'s agents-kind `stage()` for the
|
|
* opencode/kilo converters) — every fs touch below routes through
|
|
* `installFs()` (install-fs-adapter.cts), matching `retired-artifact-cleanup.cts`
|
|
* / `user-artifact-staging.cts`'s existing precedent, instead of calling
|
|
* `node:fs` directly. A raw `fs` call here silently bypassed a fake adapter
|
|
* injected via `withInstallFs`/`installRuntimeArtifacts(..., { fs })`,
|
|
* exactly the class of bug those two modules were fixed for.
|
|
*/
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
|
|
import installFsAdapter = require('./install-fs-adapter.cjs');
|
|
const { installFs } = installFsAdapter;
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-effort-resolver.cjs is an export= CommonJS module
|
|
import installEffortResolver = require('./install-effort-resolver.cjs');
|
|
const { _readGsdConfigFile, _findAncestorGsdConfigPath } = installEffortResolver as {
|
|
_readGsdConfigFile: (absPath: string, label: string) => Record<string, unknown> | null;
|
|
_findAncestorGsdConfigPath: (targetDir: string) => string | null;
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-catalog.cjs is an export= CommonJS module
|
|
import modelCatalog = require('./model-catalog.cjs');
|
|
const { MODEL_PROFILES: GSD_MODEL_PROFILES } = modelCatalog as unknown as { MODEL_PROFILES: Record<string, Record<string, string>> };
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
|
|
import modelResolverModule = require('./model-resolver.cjs');
|
|
const { resolveTierEntry: gsdResolveTierEntry, resolveModelPolicy: gsdResolveModelPolicy } = modelResolverModule as {
|
|
resolveTierEntry: (opts: { runtime: string; tier: string; overrides: unknown }) => { model?: string } | null;
|
|
resolveModelPolicy: (policy: unknown, tier: string | null | undefined) => string | null;
|
|
};
|
|
|
|
interface ReadOptions {
|
|
homedir?: () => string;
|
|
}
|
|
|
|
interface RuntimeProfileResolver {
|
|
runtime: string;
|
|
resolve: (agentName: string) => { model?: string; reasoning_effort?: string } | null;
|
|
}
|
|
|
|
/**
|
|
* Read `model_overrides` from `~/.gsd/defaults.json` at install time.
|
|
* Returns an object mapping agent names to model IDs, or null if the file
|
|
* doesn't exist or has no `model_overrides` entry.
|
|
*
|
|
* Mirrors bin/install.js's prior local `readGsdGlobalModelOverrides` exactly
|
|
* (silent try/catch — no stderr warning on malformed JSON, unlike
|
|
* `_readGsdConfigFile`'s effort-config sibling — preserved for byte-parity).
|
|
*/
|
|
function readGsdGlobalModelOverrides(options: ReadOptions = {}): Record<string, string> | null {
|
|
try {
|
|
const home = options.homedir ? options.homedir() : os.homedir();
|
|
const defaultsPath = path.join(home, '.gsd', 'defaults.json');
|
|
if (!installFs().existsSync(defaultsPath)) return null;
|
|
const raw = installFs().readFileSync(defaultsPath, 'utf-8');
|
|
const parsed = JSON.parse(raw) as { model_overrides?: unknown };
|
|
const overrides = parsed.model_overrides;
|
|
if (!overrides || typeof overrides !== 'object') return null;
|
|
return overrides as Record<string, string>;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Effective per-agent `model_overrides` for the install path.
|
|
*
|
|
* Merges `~/.gsd/defaults.json` (global) with per-project
|
|
* `<project>/.planning/config.json`. Per-project keys win on conflict; keys
|
|
* present in only one source are preserved from that source (#2256).
|
|
*
|
|
* `targetDir` is the consuming runtime's install root (walks up to find
|
|
* `.planning/`). When `targetDir` is null/undefined only the global file is
|
|
* consulted.
|
|
*
|
|
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
|
|
* source defines `model_overrides`.
|
|
*/
|
|
function readGsdEffectiveModelOverrides(targetDir: string | null = null, options: ReadOptions = {}): Record<string, string> | null {
|
|
const global = readGsdGlobalModelOverrides(options);
|
|
|
|
let projectOverrides: Record<string, string> | null = null;
|
|
if (targetDir) {
|
|
// #2875 defect fix (Generative Fix Divergence): the 8-deep upward walk to
|
|
// `.planning/config.json` is single-sourced in install-effort-resolver.cts
|
|
// (`_findAncestorGsdConfigPath`) — this module was itself extracted FROM
|
|
// that one to stop duplicating shared install-time config logic, so a
|
|
// second hand-rolled copy of the walk here defeated the point.
|
|
const candidate = _findAncestorGsdConfigPath(targetDir);
|
|
if (candidate) {
|
|
try {
|
|
const parsed = JSON.parse(installFs().readFileSync(candidate, 'utf-8')) as { model_overrides?: unknown };
|
|
if (parsed && typeof parsed === 'object' && parsed.model_overrides && typeof parsed.model_overrides === 'object') {
|
|
projectOverrides = parsed.model_overrides as Record<string, string>;
|
|
}
|
|
} catch {
|
|
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
|
|
// surfaces a parse warning via _readGsdConfigFile already.
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!global && !projectOverrides) return null;
|
|
// Per-project wins on conflict; preserve non-conflicting global keys.
|
|
return { ...(global || {}), ...(projectOverrides || {}) };
|
|
}
|
|
|
|
type AgentTools = Record<string, string[]>;
|
|
|
|
function readGsdAgentTools(config: Record<string, unknown> | null): AgentTools | null {
|
|
const raw = config?.agent_tools;
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
|
|
const result: AgentTools = {};
|
|
for (const [selector, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
// An explicitly present selector always has a verdict. Invalid values are
|
|
// empty so a project config cannot accidentally restore a global grant.
|
|
result[selector] = Array.isArray(value)
|
|
? value.filter((entry): entry is string => typeof entry === 'string')
|
|
.map((entry) => entry.trim())
|
|
.filter((entry) => entry.length > 0
|
|
&& !entry.endsWith(':')
|
|
&& !/[\s\u0000-\u001F\u007F-\u009F,#"'\u2028\u2029]/.test(entry))
|
|
: [];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Resolve valid install-time `agent_tools` grants from global defaults and
|
|
* the nearest project config. Project selectors replace only matching global
|
|
* selectors; malformed whole files remain harmless absence like the existing
|
|
* model override resolver.
|
|
*/
|
|
function readGsdEffectiveAgentTools(targetDir: string | null = null, options: ReadOptions = {}): AgentTools | null {
|
|
const home = options.homedir ? options.homedir() : os.homedir();
|
|
const globalConfig = _readGsdConfigFile(path.join(home, '.gsd', 'defaults.json'), 'global defaults');
|
|
|
|
let projectConfig: Record<string, unknown> | null = null;
|
|
if (targetDir) {
|
|
const candidate = _findAncestorGsdConfigPath(targetDir);
|
|
if (candidate) {
|
|
projectConfig = _readGsdConfigFile(candidate, 'project config');
|
|
}
|
|
}
|
|
|
|
const global = readGsdAgentTools(globalConfig);
|
|
const project = readGsdAgentTools(projectConfig);
|
|
if (projectConfig
|
|
&& Object.prototype.hasOwnProperty.call(projectConfig, 'agent_tools')
|
|
&& project === null) return {};
|
|
if (!global && !project) return null;
|
|
return { ...(global || {}), ...(project || {}) };
|
|
}
|
|
|
|
interface RuntimeProfileMergedConfig {
|
|
runtime: string | null;
|
|
model_profile: string;
|
|
model_profile_overrides: unknown;
|
|
/**
|
|
* #3705: the provider-neutral policy (#49). Install-time never read it, so a
|
|
* project configured for a non-Anthropic provider had its agent frontmatter
|
|
* rebaked to catalog `anthropic/claude-*` IDs on every update while
|
|
* dispatch-time resolution honoured the policy correctly — two answers from
|
|
* one config, and only the frontmatter is what a spawn actually uses.
|
|
*/
|
|
model_policy: unknown;
|
|
}
|
|
|
|
/**
|
|
* Build a runtime-aware tier resolver for the install path (#2517).
|
|
*
|
|
* Probes BOTH per-project `<targetDir>/.planning/config.json` AND
|
|
* `~/.gsd/defaults.json`, with per-project keys winning over global.
|
|
*
|
|
* Returns null if no `runtime` is configured, if `model_profile` is
|
|
* `inherit`, or if no project config is reachable AND `~/.gsd/defaults.json`
|
|
* declares no `model_profile` (#3543 — an unverifiable profile is never
|
|
* baked, letting the runtime's own default/session model govern).
|
|
*/
|
|
function readGsdRuntimeProfileResolver(targetDir: string | null = null): RuntimeProfileResolver | null {
|
|
const homeDefaults = _readGsdConfigFile(
|
|
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
|
'~/.gsd/defaults.json',
|
|
);
|
|
|
|
// #2875 defect fix (Generative Fix Divergence): same shared walk as
|
|
// readGsdEffectiveModelOverrides above — see that call site's comment.
|
|
let projectConfig: Record<string, unknown> | null = null;
|
|
if (targetDir) {
|
|
const candidate = _findAncestorGsdConfigPath(targetDir);
|
|
if (candidate) projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
|
}
|
|
|
|
const merged: RuntimeProfileMergedConfig = {
|
|
runtime:
|
|
(projectConfig && (projectConfig.runtime as string | undefined)) ||
|
|
(homeDefaults && (homeDefaults.runtime as string | undefined)) ||
|
|
null,
|
|
model_profile:
|
|
(projectConfig && (projectConfig.model_profile as string | undefined)) ||
|
|
(homeDefaults && (homeDefaults.model_profile as string | undefined)) ||
|
|
'balanced',
|
|
model_profile_overrides:
|
|
(projectConfig && projectConfig.model_profile_overrides) ||
|
|
(homeDefaults && homeDefaults.model_profile_overrides) ||
|
|
null,
|
|
// #3705: same project-wins-over-home precedence as every other key here.
|
|
model_policy:
|
|
(projectConfig && projectConfig.model_policy) ||
|
|
(homeDefaults && homeDefaults.model_policy) ||
|
|
null,
|
|
};
|
|
|
|
if (!merged.runtime) return null;
|
|
|
|
if (!projectConfig && !(homeDefaults && homeDefaults.model_profile)) {
|
|
return null;
|
|
}
|
|
|
|
const profile = String(merged.model_profile).toLowerCase();
|
|
if (profile === 'inherit') return null;
|
|
|
|
const runtime = merged.runtime;
|
|
return {
|
|
runtime,
|
|
resolve(agentName: string) {
|
|
const agentModels = GSD_MODEL_PROFILES[agentName];
|
|
if (!agentModels) return null;
|
|
const tier = agentModels[profile] || agentModels.balanced;
|
|
if (!tier) return null;
|
|
// #3705: policy sits between the per-agent override and the tier table —
|
|
// the position dispatch uses. Measured against `query resolve-model` on the
|
|
// same config: policy-only -> the policy model; tier-overrides-only -> the
|
|
// override; BOTH -> the policy model. So policy outranks
|
|
// `model_profile_overrides`, and an explicit `model_overrides[agent]`
|
|
// (applied by resolveAgentModelOverride below) outranks policy.
|
|
//
|
|
// Calls the exported owner (#49) rather than re-deriving tier -> model here:
|
|
// a second implementation of that mapping is the very divergence this issue
|
|
// is, one layer down. A null from it (unknown provider, missing tier key, a
|
|
// `runtime_tiers` miss) falls through to the tier table — never to null,
|
|
// which would omit a frontmatter key that used to be written.
|
|
// The policy object must carry the effective runtime before it is resolved.
|
|
// `resolveModelPolicy`'s `runtime_tiers` branch reads `policy['runtime']`, but
|
|
// the documented config shape (docs/CONFIGURATION.md) puts `runtime` at the
|
|
// TOP level and keeps only `provider`/`runtime_tiers` inside `model_policy`.
|
|
// Dispatch injects it — `{ ...config.model_policy, runtime: effectiveRuntime }`
|
|
// (model-resolver.cts) — and passing the policy unmodified here silently
|
|
// skipped `runtime_tiers` entirely, falling through to the flat hi/med/lo keys
|
|
// or the catalog tier. That is the same "catalog Anthropic ID baked over a
|
|
// configured provider" defect this fix exists to close, so the injection is
|
|
// mirrored rather than assumed.
|
|
const policyForRuntime = merged.model_policy
|
|
? { ...(merged.model_policy as Record<string, unknown>), runtime }
|
|
: null;
|
|
const policyModel = gsdResolveModelPolicy(policyForRuntime, tier);
|
|
if (policyModel) return { model: policyModel };
|
|
return gsdResolveTierEntry({
|
|
runtime,
|
|
tier,
|
|
overrides: merged.model_profile_overrides,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve the effective model override for a single agent, given a
|
|
* pre-resolved `modelOverrides` map and `runtimeResolver` (both from the
|
|
* functions above). Pure — no filesystem access.
|
|
*
|
|
* Precedence (J8 — identical for kilo and opencode, resolved through this ONE
|
|
* shared function so the two runtimes can never diverge):
|
|
* 1. modelOverrides[agentName] (#2256 — explicit per-agent override)
|
|
* 2. runtimeResolver.resolve(agentName)?.model
|
|
* (#2794 — tier-based model_profile_overrides.<runtime>.<tier>)
|
|
* 3. null (omit — J7: the frontmatter key must not appear, not `null`/`""`)
|
|
*/
|
|
function resolveAgentModelOverride(
|
|
agentName: string,
|
|
modelOverrides: Record<string, string> | null | undefined,
|
|
runtimeResolver: RuntimeProfileResolver | null | undefined,
|
|
): string | null {
|
|
const explicit = modelOverrides ? modelOverrides[agentName] : undefined;
|
|
if (explicit) return explicit;
|
|
if (runtimeResolver) {
|
|
const entry = runtimeResolver.resolve(agentName);
|
|
if (entry && entry.model) return entry.model;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export = {
|
|
readGsdGlobalModelOverrides,
|
|
readGsdEffectiveModelOverrides,
|
|
readGsdEffectiveAgentTools,
|
|
readGsdRuntimeProfileResolver,
|
|
resolveAgentModelOverride,
|
|
};
|