@mastra/core
Version:
378 lines (377 loc) • 13.7 kB
JavaScript
import { a as isProviderRegistered, i as getRegisteredProviders, l as parseModelString } from "../../provider-registry-DOB2blrv.js";
//#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 = 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 = 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 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
export { BUILDER_FEATURE_DEFAULTS, MODEL_NOT_ALLOWED_CODE, ModelNotAllowedError, assertModelAllowed, builderToModelPolicy, enforceModelAllowlist, isBuilderModelPolicyActive, isModelAllowed, isModelNotAllowedError, matchesProvider, resolveAgentFeatures, resolvePickerVisibility, toModelCandidates };
//# sourceMappingURL=index.js.map