Files
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

177 lines
9.0 KiB
JavaScript

// REQ-LIMIT-1: editable concurrency policy, never a compiled-in capacity ceiling.
import { requireObject, requireId, requireString, rejectUnknownKeys } from "./util.mjs";
import { isDailyModelQuota } from "../../plugins/src/model-quota.mjs";
import { BusinessError, refuse } from "./errors.mjs";
const name = (value, at) => requireString(value, at, { max: 200 });
const count = (value, at) => {
if (!Number.isSafeInteger(value) || value < 0) refuse(`${at} must be a whole number of 0 or more`);
return value;
};
const own = (object, key) => Object.hasOwn(object ?? {}, key) ? object[key] : undefined;
export function checkModels(value, where = "launch.models") {
requireObject(value, where);
if (!Object.keys(value).length) refuse(`${where} must name at least one model`);
return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
requireId(key, where);
requireObject(entry, `${where}.${key}`);
const moduleBacked = Object.hasOwn(entry, "module") || Object.hasOwn(entry, "upstream");
rejectUnknownKeys(entry, moduleBacked ? ["provider", "model", "module", "upstream"] : entry.provider === "proxy" ? ["provider", "model", "route"] : ["provider", "model"], `${where}.${key}`);
if (moduleBacked) {
requireId(entry.module, `${where}.${key}.module`);
requireId(entry.upstream, `${where}.${key}.upstream`);
if (typeof entry.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(entry.model)) refuse(`${where}.${key}.model is invalid`);
}
if (entry.provider === "proxy" && !moduleBacked) {
if (!Object.hasOwn(entry, "route")) throw Object.assign(new BusinessError(`${where}.${key}.route is required`), { code: "proxy-route-missing" });
if (typeof entry.route !== "string" || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(entry.route)) throw Object.assign(new BusinessError(`${where}.${key}.route is invalid`), { code: "proxy-route-unknown" });
}
return [key, {
provider: name(entry.provider, `${where}.${key}.provider`),
model: name(entry.model, `${where}.${key}.model`),
...(moduleBacked ? { module: entry.module, upstream: entry.upstream } : {}),
...(entry.provider === "proxy" && !moduleBacked ? { route: entry.route } : {}),
}];
}));
}
// Row 125 (#1619, REQ-LIMIT-3): one token limit, a count of 0 or more and the
// canonical time it was set or last reset. The settings writer stamps `since`.
const SINCE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const tokenLimit = (value, at) => {
requireObject(value, at);
rejectUnknownKeys(value, ["limit", "since"], at);
const since = value.since;
if (typeof since !== "string" || !SINCE.test(since) || Number.isNaN(Date.parse(since)) ||
new Date(since).toISOString() !== since) refuse(`${at}.since must be a canonical timestamp`);
return { limit: count(value.limit, `${at}.limit`), since };
};
// Row 126 (#1620, REQ-LIMIT-2): a daily quota is a token limit with the wall-clock time its day
// starts and the IANA zone that time is read in. A zone must be a name (an offset such as +05:00
// is refused) that Intl accepts.
const RESET = /^([01]\d|2[0-3]):[0-5]\d$/;
const ZONE = /^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+)*$/;
const dailyQuota = (value, at) => {
requireObject(value, at);
rejectUnknownKeys(value, ["limit", "since", "reset", "timeZone"], at);
const { limit, since } = tokenLimit({ limit: value.limit, since: value.since }, at);
if (typeof value.reset !== "string" || !RESET.test(value.reset)) refuse(`${at}.reset must be a time of day, HH:MM`);
const timeZone = value.timeZone;
let known = typeof timeZone === "string" && ZONE.test(timeZone);
if (known) {
try { new Intl.DateTimeFormat("en-US", { timeZone }); } catch { known = false; }
}
if (!known) refuse(`${at}.timeZone must be an IANA time zone name`);
return { limit, since, reset: value.reset, timeZone };
};
const tokenMap = (value, at, key, each) => {
requireObject(value, at);
return Object.fromEntries(Object.entries(value).map(([k, v]) => [key(k, at), each(v, `${at}.${k}`)]));
};
// launch.limits.tokens: instances, providers, models (per provider), keys and daily, each optional.
export function checkTokenLimits(value, where = "launch.limits.tokens") {
requireObject(value, where);
rejectUnknownKeys(value, ["instances", "providers", "models", "keys", "daily"], where);
const out = {};
if (Object.hasOwn(value, "instances")) out.instances = tokenMap(value.instances, `${where}.instances`, requireId, tokenLimit);
if (Object.hasOwn(value, "providers")) out.providers = tokenMap(value.providers, `${where}.providers`, name, tokenLimit);
if (Object.hasOwn(value, "models")) {
out.models = tokenMap(value.models, `${where}.models`, name, (models, at) => tokenMap(models, at, name, tokenLimit));
}
// P5 adds daily model leaves, including digit-leading IDs; plain keys keep
// their existing account-name schema and cumulative account semantics.
if (Object.hasOwn(value, "keys")) {
requireObject(value.keys, `${where}.keys`);
out.keys = Object.fromEntries(Object.entries(value.keys).map(([key, entry]) => {
const at = `${where}.keys.${key}`;
if (isDailyModelQuota(key, entry)) return [key, dailyQuota(entry, at)];
return [requireId(key, `${where}.keys`), tokenLimit(entry, at)];
}));
}
// One quota per account; the business read checks each key names launch.account.
if (Object.hasOwn(value, "daily")) out.daily = tokenMap(value.daily, `${where}.daily`, requireId, dailyQuota);
return out;
}
// Row 126 (#1620): the account a business's claude-code launches use. `credential` names the
// environment variables of one credential set, joined with "+", so a secret can't pass the check.
// Which sets exist is the launcher's to check (seat imports business, not the other way).
const CREDENTIAL = /^[A-Z][A-Z0-9_]*(\+[A-Z][A-Z0-9_]*)*$/;
export function checkAccount(value, where = "launch.account") {
requireObject(value, where);
rejectUnknownKeys(value, ["name", "credential"], where);
const accountName = requireId(value.name, `${where}.name`);
if (typeof value.credential !== "string" || (value.credential !== "proxy" && !CREDENTIAL.test(value.credential))) {
refuse(`${where}.credential must name environment variables, joined with "+"`);
}
return { name: accountName, credential: value.credential };
}
export function checkLimits(value = {}, where = "launch.limits") {
requireObject(value, where);
rejectUnknownKeys(value, ["total", "providers", "models", "tokens"], where);
const out = {};
if (Object.hasOwn(value, "total")) out.total = count(value.total, `${where}.total`);
if (Object.hasOwn(value, "providers")) {
requireObject(value.providers, `${where}.providers`);
out.providers = Object.fromEntries(Object.entries(value.providers).map(([provider, limit]) => {
return [name(provider, where), count(limit, `${where}.providers.${provider}`)];
}));
}
if (Object.hasOwn(value, "models")) {
requireObject(value.models, `${where}.models`);
out.models = Object.fromEntries(Object.entries(value.models).map(([provider, models]) => {
name(provider, where);
requireObject(models, where);
return [provider, Object.fromEntries(Object.entries(models).map(([model, limit]) => {
return [name(model, where), count(limit, `${where}.models.${provider}.${model}`)];
}))];
}));
}
if (Object.hasOwn(value, "tokens")) out.tokens = checkTokenLimits(value.tokens, `${where}.tokens`);
return out;
}
export function usageCounts(sessions, business) {
const running = sessions.filter(session => session.business === business);
const providerCounts = new Map();
const modelCounts = new Map();
for (const session of running) {
const { provider, model } = session;
if (typeof provider !== "string" || typeof model !== "string") {
refuse("running session has no exact provider/model; reconcile before launching");
}
providerCounts.set(provider, (providerCounts.get(provider) ?? 0) + 1);
if (!modelCounts.has(provider)) modelCounts.set(provider, new Map());
const models = modelCounts.get(provider);
models.set(model, (models.get(model) ?? 0) + 1);
}
const providers = Object.fromEntries(providerCounts);
const models = Object.fromEntries([...modelCounts].map(([provider, counts]) => {
return [provider, Object.fromEntries(counts)];
}));
return { total: running.length, providers, models };
}
export function limitHit(sessions, business, limits, selected) {
const usage = usageCounts(sessions, business);
const { provider, model } = selected;
const checks = [
["total", limits.total, usage.total],
["provider", own(limits.providers, provider), own(usage.providers, provider) ?? 0],
["model", own(own(limits.models, provider), model), own(own(usage.models, provider), model) ?? 0],
];
for (const [scope, limit, running] of checks) {
if (limit !== undefined && running >= limit) {
return {
scope, limit, running,
...(scope !== "total" ? { provider } : {}),
...(scope === "model" ? { model } : {}),
};
}
}
return null;
}