// The resolver (REQ-VAR-1 and 2). It joins one role instance's definition, // the business file, the optional project file and the system config into // the record a launcher or the broker uses. Plain values: the most specific // layer wins. Limits: every layer narrows, nothing widens. import { refuse } from "./errors.mjs"; import { ACTIONS, NETWORKS } from "./vocabulary.mjs"; import { checkVars, mergeVars } from "./vars.mjs"; import { canonicalJson, sha256, deepFreeze } from "./util.mjs"; import { projectFilePath } from "./project.mjs"; // The system layer from `mosaic-config.mjs validate` output. export function systemVars(config) { return checkVars({ environment: config.environment, dataRoot: config.dataRoot, "execution.backend": config.execution.backend, "execution.provider": config.execution.provider, "execution.model": config.execution.model, "execution.adapter": config.execution.adapter, }, "system", "system config"); } function narrowerNetwork(a, b) { return NETWORKS.indexOf(a) <= NETWORKS.indexOf(b) ? a : b; } // resolveInstance({ system, business, project, instance }) // system systemVars(...) output // business loadBusiness(...) output // project loadProject(...) output, or null // instance a role instance the business file declares export function resolveInstance({ system, business, project = null, instance }) { const entry = Object.hasOwn(business.roles, instance) ? business.roles[instance] : null; if (!entry) refuse(`business ${business.id} declares no role instance ${JSON.stringify(instance)}`); const definition = business.definitions[entry.definition]; const layers = [ { layer: "system", source: "system", vars: system }, { layer: "business", source: `business:${business.id}`, vars: business.vars }, ]; let projectId = null; if (project) { const declared = Object.entries(business.projects).find(([, p]) => projectFilePath(p.root) === project.file); if (!declared || declared[0] !== project.id) { refuse(`project ${project.id} (${project.file}) isn't declared under that id in business ${business.id}`); } projectId = project.id; for (const name of Object.keys(project.roles)) { if (!Object.hasOwn(business.roles, name)) refuse(`project ${project.id} sets vars for role instance ${name}, which business ${business.id} doesn't declare`); } layers.push({ layer: "project", source: `project:${project.id}`, vars: project.vars }); if (Object.hasOwn(project.roles, instance)) { layers.push({ layer: "project", source: `project:${project.id}:roles.${instance}`, vars: project.roles[instance].vars }); } } layers.push({ layer: "agent", source: `business:${business.id}:roles.${instance}`, vars: entry.vars }); const { vars, provenance } = mergeVars(layers); let tools = [...definition.tools]; let network = definition.network; let withinRole = [...definition.authority.withinRole]; let crossRole = [...definition.authority.crossRole]; if (vars["limits.tools"]) tools = tools.filter((t) => vars["limits.tools"].includes(t)); if (vars["limits.network"]) network = narrowerNetwork(network, vars["limits.network"]); if (vars["limits.authority"]) { withinRole = withinRole.filter((a) => vars["limits.authority"].includes(a)); crossRole = crossRole.filter((a) => vars["limits.authority"].includes(a)); } // role.launch needs the business file's launch block naming this // instance (addendum A section 8), and the verb must survive // limits.authority. Otherwise the verb is gated and `launch` is null, // so the two never disagree. const launch = business.launch && business.launch.by === instance && withinRole.includes("role.launch") ? business.launch : null; if (!launch) { withinRole = withinRole.filter((a) => a !== "role.launch"); crossRole = crossRole.filter((a) => a !== "role.launch"); } const resolved = { business: business.id, project: projectId, instance, definition: definition.name, holder: entry.holder, contract: definition.contractPath, vars, provenance, limits: { tools, network, authority: { withinRole, crossRole } }, credentials: entry.credentials, tracker: entry.tracker, launch, }; return deepFreeze({ ...resolved, digest: sha256(canonicalJson(resolved)) }); } // Classify one action for a resolved instance: "within", "cross" or // "gated". An action outside the vocabulary refuses. export function classify(resolved, action) { if (!ACTIONS.includes(action)) refuse(`unknown action: ${JSON.stringify(action)}`); if (resolved.limits.authority.withinRole.includes(action)) return "within"; if (resolved.limits.authority.crossRole.includes(action)) return "cross"; return "gated"; }