UNPKG

@mastra/core

Version:
391 lines (390 loc) 14.2 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const require_provider_registry = require("../../provider-registry-Bv8eMxuW.cjs"); //#region src/agent-builder/ee/types.ts /** * Default-on values for {@link AgentFeatures}. `browser` is defaulted * dynamically by {@link resolveAgentFeatures} based on configuration; it is * intentionally absent here so the shape mirrors what `resolveAgentFeatures` * unconditionally fills in. */ const BUILDER_FEATURE_DEFAULTS = { tools: true, agents: true, workflows: true, scorers: true, skills: true, memory: true, variables: true, favorites: true, avatarUpload: true, model: true }; /** * Pure normalization of the raw {@link AgentFeatures} into a fully-populated * shape with default-on semantics applied. * * Rules: * - Explicit `false` always wins (admin opt-out). * - Explicit `true` wins for non-`browser` keys. * - Omitted keys resolve to `true` (except `browser`, see below). * - `browser`: * - explicit `false` ⇒ `false`. * - explicit `true` + `hasBrowserConfig: false` ⇒ `false` (caller is * responsible for emitting a warning; this helper does not throw). * - explicit `true` + `hasBrowserConfig: true` ⇒ `true`. * - omitted ⇒ `hasBrowserConfig` (default-on only when prerequisite met). */ function resolveAgentFeatures(raw, ctx) { const pick = (key) => { const explicit = raw?.[key]; return explicit === void 0 ? BUILDER_FEATURE_DEFAULTS[key] : explicit; }; const resolveBrowser = () => { const explicit = raw?.browser; if (explicit === false) return false; if (explicit === true) return ctx.hasBrowserConfig; return ctx.hasBrowserConfig; }; return { tools: pick("tools"), agents: pick("agents"), workflows: pick("workflows"), scorers: pick("scorers"), skills: pick("skills"), memory: pick("memory"), variables: pick("variables"), favorites: pick("favorites"), avatarUpload: pick("avatarUpload"), model: pick("model"), browser: resolveBrowser() }; } //#endregion //#region src/agent-builder/ee/errors.ts const MODEL_NOT_ALLOWED_CODE = "MODEL_NOT_ALLOWED"; /** * Thrown by `enforceModelAllowlist` call sites when a write attempts to persist * a model that the active builder allowlist does not permit. * * Lives in `@mastra/core` so editor and server layers can both throw it * without crossing package boundaries. The server adapter * (`packages/server/src/server/handlers/error.ts`) maps this to HTTP 422 with * a structured JSON body of the same shape. */ var ModelNotAllowedError = class extends Error { code = MODEL_NOT_ALLOWED_CODE; allowed; attempted; offendingLabel; constructor(args) { const message = args.message ?? `Model "${args.attempted.provider}/${args.attempted.modelId}" (${args.offendingLabel}) is not in the configured allowlist.`; super(message); this.name = "ModelNotAllowedError"; this.allowed = args.allowed; this.attempted = args.attempted; this.offendingLabel = args.offendingLabel; } }; function isModelNotAllowedError(error) { return error instanceof Error && error.code === "MODEL_NOT_ALLOWED"; } //#endregion //#region src/agent-builder/ee/normalize-candidate.ts /** * Gateway-aware split of a runtime model string. `parseModelString` only splits * on the first slash, which fails for gateway provider IDs that themselves * contain a slash (e.g. `acme/custom/foo-1`). We try the longest registered * provider prefix first and fall back to the first-slash split when no match * is found in the registry. */ function splitRuntimeModelString(input) { const providers = require_provider_registry.getRegisteredProviders().sort((a, b) => b.length - a.length); for (const providerId of providers) { const prefix = `${providerId}/`; if (input.startsWith(prefix)) { const modelId = input.slice(prefix.length); if (modelId.length > 0) return { provider: providerId, modelId }; } } const parsed = require_provider_registry.parseModelString(input); if (parsed.provider && parsed.modelId) return { provider: parsed.provider, modelId: parsed.modelId }; } function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function fromObject(value, origin, label) { const providerField = value.provider; const modelIdField = value.modelId; const nameField = value.name; const idField = value.id; const providerIdField = value.providerId; if (typeof idField === "string" && idField.includes("/") && providerField === void 0) { const split = splitRuntimeModelString(idField); if (split) return [{ ...split, origin: "openai-compatible", label: label ?? idField }]; } if (typeof providerIdField === "string" && typeof modelIdField === "string") return [{ provider: providerIdField, modelId: modelIdField, origin: "openai-compatible", label: label ?? `${providerIdField}/${modelIdField}` }]; if (typeof providerField === "string" && typeof modelIdField === "string") return [{ provider: providerField, modelId: modelIdField, origin: typeof value.doGenerate === "function" ? "sdk-instance" : origin, label: label ?? `${providerField}/${modelIdField}` }]; if (typeof providerField === "string" && typeof nameField === "string") return [{ provider: providerField, modelId: nameField, origin, label: label ?? `${providerField}/${nameField}` }]; return []; } /** * Convert any supported model expression into a flat list of `{ provider, modelId }` * candidates. Empty array means "could not statically determine" — callers * should treat that as unenforced at this level (runtime defense in Phase 7 * picks it up). * * Dispatch order: * 1. `null` / `undefined` / `function` → `[]` (dynamic, defer to runtime) * 2. `string` → gateway-aware split * 3. Conditional variants array → walk each variant * 4. Object → openai-compatible / SDK instance / stored static, see {@link fromObject} */ function toModelCandidates(input) { if (input === null || input === void 0) return []; if (typeof input === "function") return []; if (typeof input === "string") { const split = splitRuntimeModelString(input); if (!split) return []; return [{ ...split, origin: "runtime", label: input }]; } if (Array.isArray(input)) { const candidates = []; input.forEach((variant, index) => { if (!isPlainObject(variant)) return; const value = variant.value ?? variant; const hasRules = isPlainObject(variant) && "rules" in variant && variant.rules != null; const origin = hasRules ? "conditional-variant" : "conditional-default"; const label = hasRules ? `variant[${index}]` : `variant[${index}] (default)`; if (typeof value === "string") { const split = splitRuntimeModelString(value); if (split) candidates.push({ ...split, origin, label }); return; } if (isPlainObject(value)) candidates.push(...fromObject(value, origin, label)); }); return candidates; } if (isPlainObject(input)) return fromObject(input, "static"); return []; } //#endregion //#region src/agent-builder/ee/allowlist.ts /** * Single-entry match: provider equality (case-sensitive). When the entry omits * `modelId` it matches every model under that provider (provider wildcard). * * Custom (`kind: 'custom'`) entries match by exact provider string. Known-provider * entries match by exact provider string too — the typed surface is purely a * compile-time guard. */ function matchesProvider(entry, candidate) { if (entry.provider !== candidate.provider) return false; if (!entry.modelId) return true; return entry.modelId === candidate.modelId; } /** * Returns `true` if the candidate is allowed under the given allowlist. * * Rules: * - `undefined` allowlist ⇒ unrestricted (always `true`). * - `[]` empty allowlist ⇒ unrestricted (always `true`). * - Non-empty allowlist where **every** entry's provider is unknown to the * runtime registry AND not tagged `kind: 'custom'` ⇒ deny everything. This * prevents typos (e.g. `openaii`) from acting as an unintended deny-all that * silently allows anything else; it is the documented "deny vs ignore" rule. */ function isModelAllowed(allowed, candidate) { if (allowed === void 0) return true; if (allowed.length === 0) return true; const activeEntries = allowed.filter((entry) => { if ("kind" in entry && entry.kind === "custom") return true; return require_provider_registry.isProviderRegistered(entry.provider); }); if (activeEntries.length === 0) return false; return activeEntries.some((entry) => matchesProvider(entry, candidate)); } /** * Apply an allowlist to any supported model expression. Normalizes via * `toModelCandidates`, then runs `isModelAllowed` per candidate. Returns the * **first** failing candidate so error messages can pinpoint which variant of * a conditional / fallback list violated the policy. * * If `toModelCandidates` returns no candidates (dynamic function, unparsable * shape) this passes — runtime defense (Phase 7) handles those cases. */ function enforceModelAllowlist(allowed, input) { const candidates = toModelCandidates(input); for (const candidate of candidates) if (!isModelAllowed(allowed, candidate)) return { ok: false, attempted: candidate, offendingLabel: candidate.label ?? candidate.origin }; return { ok: true }; } /** * Convenience wrapper around `enforceModelAllowlist` that throws * `ModelNotAllowedError` on rejection. Use at write call sites so the server * adapter can translate into HTTP 422 + structured body. */ function assertModelAllowed(allowed, input) { const result = enforceModelAllowlist(allowed, input); if (result.ok) return; throw new ModelNotAllowedError({ allowed, attempted: result.attempted, offendingLabel: result.offendingLabel }); } //#endregion //#region src/agent-builder/ee/policy.ts /** * Single source of truth for whether the admin has actually configured a model * policy. Reused by: * - {@link builderToModelPolicy} (UI / runtime derivation) * - `EditorAgentBuilder` config validation (Phase 4) * - Server-side enforcement gate (Phase 6) * * "Active" means the admin opted into the model slice in some way: * - the picker is visible (open-mode), OR * - an allowlist was set, OR * - a default model was set. * * If the builder is `enabled: false`, the slice is never active. */ function isBuilderModelPolicyActive(inputs) { if (!inputs.enabled) return false; if (inputs.pickerVisible) return true; if (inputs.allowed !== void 0) return true; if (inputs.default !== void 0) return true; return false; } /** * Pure derivation of the {@link BuilderModelPolicy} from an `IAgentBuilder`. * No `Mastra` / `IEditor` dependency — server and editor wrappers feed it * a builder instance through their own resolution paths. * * Returns `{ active: false }` when: * - the builder is missing, * - the builder is disabled, or * - none of the model-slice signals are present. * * In every active case, `allowed` and `default` are passed through verbatim * so locked-mode UI still has the data it needs to render the chosen model. */ function builderToModelPolicy(builder) { if (!builder || !builder.enabled) return { active: false }; const features = builder.getFeatures(); const configuration = builder.getConfiguration(); const pickerVisible = features?.agent?.model === true; const models = configuration?.agent?.models; const allowed = models?.allowed; const defaultModel = models?.default; if (!isBuilderModelPolicyActive({ enabled: builder.enabled, pickerVisible, allowed, default: defaultModel })) return { active: false }; return { active: true, pickerVisible, ...allowed !== void 0 ? { allowed } : {}, ...defaultModel !== void 0 ? { default: defaultModel } : {} }; } //#endregion //#region src/agent-builder/ee/picker.ts function resolveOne(allowlist, registered, kindLabel, configPath) { if (allowlist === void 0) return { visible: null, warnings: [] }; const known = new Set(registered); const seen = /* @__PURE__ */ new Set(); const visible = []; const warnings = []; for (const id of allowlist) { if (seen.has(id)) continue; seen.add(id); if (known.has(id)) visible.push(id); else warnings.push(`${configPath} references unknown ${kindLabel} "${id}" — no ${kindLabel} with this ID is registered. It will be hidden from the builder picker.`); } return { visible, warnings }; } /** * Pure derivation of {@link ResolvedPickerVisibility} from admin config and * the registered tool/agent/workflow sets. * * Per kind: * - allowlist undefined ⇒ `null` (unrestricted), no warnings. * - allowlist provided ⇒ filter to known IDs; emit one warning per unknown ID. * * Stable order: each visible list preserves admin-provided order with unknowns * dropped. Duplicates are de-duplicated. */ function resolvePickerVisibility({ config, registeredToolIds, registeredAgentIds, registeredWorkflowIds }) { const tools = resolveOne(config?.tools?.allowed, registeredToolIds, "tool", "configuration.agent.tools.allowed"); const agents = resolveOne(config?.agents?.allowed, registeredAgentIds, "agent", "configuration.agent.agents.allowed"); const workflows = resolveOne(config?.workflows?.allowed, registeredWorkflowIds, "workflow", "configuration.agent.workflows.allowed"); return { visibleTools: tools.visible, visibleAgents: agents.visible, visibleWorkflows: workflows.visible, warnings: [ ...tools.warnings, ...agents.warnings, ...workflows.warnings ] }; } //#endregion exports.BUILDER_FEATURE_DEFAULTS = BUILDER_FEATURE_DEFAULTS; exports.MODEL_NOT_ALLOWED_CODE = MODEL_NOT_ALLOWED_CODE; exports.ModelNotAllowedError = ModelNotAllowedError; exports.assertModelAllowed = assertModelAllowed; exports.builderToModelPolicy = builderToModelPolicy; exports.enforceModelAllowlist = enforceModelAllowlist; exports.isBuilderModelPolicyActive = isBuilderModelPolicyActive; exports.isModelAllowed = isModelAllowed; exports.isModelNotAllowedError = isModelNotAllowedError; exports.matchesProvider = matchesProvider; exports.resolveAgentFeatures = resolveAgentFeatures; exports.resolvePickerVisibility = resolvePickerVisibility; exports.toModelCandidates = toModelCandidates; //# sourceMappingURL=index.cjs.map