/** Arithmetic preflight only. No Docker/host access, changes or workload admission. * Roles are operator-supplied labels, not authenticated service identities. * Even a fitting budget cannot prove runtime <=2GB or permission to change limits. */ const MiB = 1024 * 1024 const roles = new Set(['backend', 'postgres', 'frontend', 'portal', 'proxy', 'agent', 'other']) const singleton = new Set(['backend', 'postgres', 'frontend', 'portal']) const integer = value => Number.isSafeInteger(value) && value >= 0 const exact = (value, fields) => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).sort().join('|') === [...fields].sort().join('|') const id = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value) export function evaluateInstallationMemoryBudget(input) { if (!exact(input, ['hostMemTotalBytes', 'osReserveBytes', 'observedContainerIds', 'containers']) || !integer(input.hostMemTotalBytes) || !integer(input.osReserveBytes) || !Array.isArray(input.observedContainerIds) || !Array.isArray(input.containers) || input.observedContainerIds.length > 128 || input.containers.length > 128) throw Error('Invalid bounded installation inventory') if (input.observedContainerIds.some(value => !id(value)) || new Set(input.observedContainerIds).size !== input.observedContainerIds.length) throw Error('Invalid observed container set') const seen = new Set(), counts = new Map(), reasons = new Set() let configuredBytes = 0 for (const row of input.containers) { if (!exact(row, ['id', 'role', 'configuredLimitBytes']) || !id(row.id) || !roles.has(row.role) || !integer(row.configuredLimitBytes) || seen.has(row.id)) throw Error('Invalid container budget') seen.add(row.id); counts.set(row.role, (counts.get(row.role) ?? 0) + 1) if (row.configuredLimitBytes === 0) reasons.add('UNBOUNDED_CONTAINER') if (row.configuredLimitBytes > input.hostMemTotalBytes) reasons.add('CONTAINER_LIMIT_EXCEEDS_HOST') if (!Number.isSafeInteger(configuredBytes + row.configuredLimitBytes)) throw Error('Container budget overflow') configuredBytes += row.configuredLimitBytes } if (seen.size !== input.observedContainerIds.length || input.observedContainerIds.some(value => !seen.has(value))) reasons.add('INCOMPLETE_HOST_CONTAINER_INVENTORY') for (const role of ['backend', 'postgres', 'frontend']) if (!counts.has(role)) reasons.add('REQUIRED_SERVICE_MISSING') for (const role of singleton) if ((counts.get(role) ?? 0) > 1) reasons.add('DUPLICATE_APPLICATION_STACK') if (input.hostMemTotalBytes === 0 || input.hostMemTotalBytes > 2 * 1024 * MiB) reasons.add('HOST_OUTSIDE_2GIB_TARGET') if (input.osReserveBytes < 512 * MiB) reasons.add('OS_RESERVE_BELOW_CANDIDATE_FLOOR') if (!Number.isSafeInteger(configuredBytes + input.osReserveBytes)) throw Error('Installation budget overflow') const plannedBytes = configuredBytes + input.osReserveBytes if (plannedBytes > input.hostMemTotalBytes) reasons.add('INSTALLATION_BUDGET_EXCEEDS_HOST') return Object.freeze({ scope: 'arithmetic-preflight-only', budgetFits: reasons.size === 0, wholeInstallation2GbAccepted: false, runtimeAcceptanceClaimed: false, changesAuthorized: false, configuredContainerBytes: configuredBytes, osReserveBytes: input.osReserveBytes, plannedBytes, remainingBytes: input.hostMemTotalBytes - plannedBytes, observedContainers: input.observedContainerIds.length, reasons: Object.freeze([...reasons].sort()) }) } /** Project the whitelist-only read-only inventory. Never substitute idle RSS or * Docker's reported host ceiling for HostConfig.Memory. Labels/freshness are * not authenticated here; this result must not be used as mutation authority. */ export function evaluateObservedInstallationBudget(inventory, roleById, osReserveBytes = 512 * MiB) { if (!inventory || inventory.readOnly !== true || inventory.containerSetStable !== true || inventory.environmentRead !== false || inventory.serviceChanges !== false || inventory.newFixtureStarted !== false || !Array.isArray(inventory.containers) || inventory.containers.length > 128 || !roleById || typeof roleById !== 'object' || Array.isArray(roleById)) throw Error('Invalid read-only installation inventory') const containers = inventory.containers.map(row => { if (!row || !id(row.id) || row.running !== true || !integer(row.memoryLimitBytes) || !/^sha256:[a-f0-9]{64}$/.test(row.imageId) || !Number.isFinite(Date.parse(row.startedAt))) throw Error('Incomplete configured-limit observation') const label = Object.getOwnPropertyDescriptor(roleById, row.id) if (!label || typeof label.value !== 'string' || !roles.has(label.value)) throw Error('Explicit container role missing') return { id: row.id, role: label.value, configuredLimitBytes: row.memoryLimitBytes } }) const observedContainerIds = containers.map(row => row.id) if (Object.keys(roleById).length !== observedContainerIds.length || Object.keys(roleById).some(key => !observedContainerIds.includes(key))) throw Error('Foreign or incomplete role inventory') const budget = evaluateInstallationMemoryBudget({ hostMemTotalBytes: inventory.hostMemTotalBytes, osReserveBytes, observedContainerIds, containers }) return Object.freeze({ ...budget, configuredLimitsSource: 'observed-HostConfig.Memory', roleLabelsAuthenticated: false, inventoryFreshnessProven: false }) } /** NEW pure raw-text bridge for measure-memory schema2/3. This is NOT the * separate GET-only inventory contract consumed by evaluateObserved above. * No file/container/socket access or mutation authority. Parse raw text only: * precision lost by a caller's previous JSON.parse cannot be recovered. */ export function evaluateMeasuredInstallationBudget(rawText, roleById, osReserveBytes = 512 * MiB) { if (typeof rawText !== 'string') throw Error('Raw measurement JSON text required; preparsed precision cannot be recovered') if (Buffer.byteLength(rawText, 'utf8') > 32 * 1024 * 1024) throw Error('Measurement text bound exceeded') // A narrow numeric lexical gate BEFORE JSON.parse, not a JSON parser. The // Python producer emits only integer JSON numbers (PSI averages are strings). // Reject decimal/exponent/unsafe integers even where JSON.parse would round // a fractional lexeme into a seemingly safe integer. JSON.parse checks syntax. const number = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y let quoted = false, escaped = false for (let index = 0; index < rawText.length; index++) { const char = rawText[index] if (quoted) { if (escaped) escaped = false; else if (char === '\\') escaped = true; else if (char === '"') quoted = false; continue } if (char === '"') { quoted = true; continue } if (char === '-' || char >= '0' && char <= '9') { number.lastIndex = index; const token = number.exec(rawText)?.[0] if (!token || token === '-0' || !/^-?(?:0|[1-9][0-9]*)$/.test(token) || token.replace('-', '').length > 16) throw Error('Unsafe or noninteger measurement JSON number') const value = BigInt(token) if (value < -9007199254740991n || value > 9007199254740991n) throw Error('Unsafe measurement JSON integer') index = number.lastIndex - 1 } } const data = JSON.parse(rawText) if (!data || ![2, 3].includes(data.schemaVersion) || data.readOnlyDockerAndProc !== true || data.serviceChanges !== 0 || data.summary?.acceptancePassed !== false || !Array.isArray(data.observations) || !data.observations.length || data.observations.length > 300 || !roleById || typeof roleById !== 'object' || Array.isArray(roleById)) throw Error('Unsupported or incomplete memory measurement') const nonnegativeSafe = value => Number.isSafeInteger(value) && value >= 0 const limit = value => value === 'max' || nonnegativeSafe(value) ? value : null const swap = row => row.configuredMemorySwapStatus === 'available' && Number.isSafeInteger(row.configuredMemorySwapBytes) && row.configuredMemorySwapBytes >= -1 ? row.configuredMemorySwapBytes : null const cpu = value => value && ['nr_periods', 'nr_throttled', 'throttled_usec'].every(key => nonnegativeSafe(value[key])) ? Object.fromEntries(['nr_periods', 'nr_throttled', 'throttled_usec'].map(key => [key, value[key]])) : null const pressure = host => { if (host.memoryPressure?.status !== 'available') return null const result = {} for (const key of ['some', 'full']) { const value = host.memoryPressure.metrics?.[key] if (!value || !nonnegativeSafe(value.totalMicros)) return null for (const avg of ['avg10Percent', 'avg60Percent', 'avg300Percent']) { if (typeof value[avg] !== 'string' || !/^[0-9]{1,3}\.[0-9]{2}$/.test(value[avg]) || Number(value[avg]) > 100) return null } result[key] = Object.fromEntries(['avg10Percent', 'avg60Percent', 'avg300Percent', 'totalMicros'].map(name => [name, value[name]])) } return result } let baseline, hostMemory, priorElapsed = -1, complete = data.schemaVersion === 3 const extendedObservations = [] for (const observation of data.observations) { if (!observation?.host || !nonnegativeSafe(observation.elapsedMs) || observation.elapsedMs < priorElapsed || observation.elapsedMs > 360000 || !nonnegativeSafe(observation.host.MemTotalBytes) || !observation.host.MemTotalBytes || !Array.isArray(observation.containers) || !observation.containers.length || observation.containers.length > 16) throw Error('Incomplete bounded measurement sample') priorElapsed = observation.elapsedMs hostMemory ??= observation.host.MemTotalBytes if (hostMemory !== observation.host.MemTotalBytes) throw Error('Measurement host memory changed') const ids = new Set(), identities = [], extended = [] for (const row of observation.containers) { if (!row || !id(row.id) || ids.has(row.id) || !/^sha256:[a-f0-9]{64}$/.test(row.imageId) || (typeof row.startedAt !== 'string' || !Number.isFinite(Date.parse(row.startedAt))) || row.running !== true || row.runningAfter !== true || row.identityStableDuringSample !== true || row.oomKilled !== false || row.oomKilledAfter !== false || !nonnegativeSafe(row.restartCount) || !Number.isSafeInteger(row.pid) || row.pid < 1 || row.pid > 2147483647 || !nonnegativeSafe(row.configuredLimitBytes)) throw Error('Unavailable measurement container identity or configured cap') ids.add(row.id); identities.push([row.id, row.imageId, row.startedAt, row.pid, row.restartCount, row.configuredLimitBytes, data.schemaVersion === 3 ? swap(row) : null]) if (data.schemaVersion === 3) { const metrics = row.cgroupV2?.metrics, memoryMax = limit(metrics?.['memory.max']), swapMax = limit(metrics?.['memory.swap.max']) const cpuThrottling = cpu(metrics?.['cpu.stat']), configuredMemorySwapBytes = swap(row) const available = row.cgroupV2?.status === 'available' && row.resourceLimitsStableDuringSample === true && memoryMax !== null && swapMax !== null && cpuThrottling !== null && configuredMemorySwapBytes !== null complete &&= available extended.push({id: row.id, memoryMax, swapMax, configuredMemorySwapBytes, cpuThrottling, available}) } else extended.push({id: row.id, memoryMax: null, swapMax: null, configuredMemorySwapBytes: null, cpuThrottling: null, available: false}) } identities.sort((a, b) => a[0].localeCompare(b[0])) const encoded = JSON.stringify(identities) baseline ??= encoded if (encoded !== baseline) throw Error('Measurement container set, identity or memory cap changed') const memoryPressure = data.schemaVersion === 3 ? pressure(observation.host) : null complete &&= memoryPressure !== null extendedObservations.push({elapsedMs: observation.elapsedMs, memoryPressure, containers: extended}) } const captured = JSON.parse(baseline) if (Object.keys(roleById).length !== captured.length || Object.keys(roleById).some(key => !captured.some(row => row[0] === key))) throw Error('Incomplete or foreign measured role inventory') const containers = captured.map(([identifier, _image, _start, _pid, _restart, configuredLimitBytes]) => { const descriptor = Object.getOwnPropertyDescriptor(roleById, identifier) if (!descriptor || typeof descriptor.value !== 'string' || !roles.has(descriptor.value)) throw Error('Explicit measured container role missing') return {id: identifier, role: descriptor.value, configuredLimitBytes} }) const budget = evaluateInstallationMemoryBudget({hostMemTotalBytes: hostMemory, osReserveBytes, observedContainerIds: containers.map(row => row.id), containers}) return Object.freeze({...budget, measurementSchemaVersion: data.schemaVersion, measurementSamples: data.observations.length, extendedMetricsComplete: complete, extendedObservations: Object.freeze(extendedObservations), roleLabelsAuthenticated: false, inventoryFreshnessProven: false, hostContainerInventoryComplete: false, measurementSelectedContainersOnly: true, noSwapAccepted: false, sourceGenerationAuthenticated: false}) }