Files
ECC/scripts/lib/install-state.js
T
754b8dd76c fix: make the installer runtime pass strict supply-chain vetting (#2503)
* fix: make the installer runtime pass strict supply-chain vetting

Remediate the four enterprise supply-chain vetting blockers from
affaan-m/ECC#2502 so the installer runtime (package.json + manifests +
scripts/lib/**) passes strict exact-pin evidence policy:

1. Remove the package.json `postinstall` lifecycle script (it only echoed a
   post-install banner) and move that banner to an explicit opt-in
   `npm run welcome` command. No install-time lifecycle script remains.
2. Exact-pin every dependency in package.json (dependencies + devDependencies)
   to the versions already resolved in package-lock.json; no ^/~ ranges.
3. Replace non-ASCII characters on the installer runtime script/config surface:
   em-dashes (U+2014) in scripts/lib/{path-safety,install-executor,
   install/link-rewrite}.js comments and the two "Itô" (U+00F4) occurrences in
   manifests/{install-components,install-modules}.json descriptions become
   ASCII, so strict-surface Unicode scanners are clean.
4. Drop the bare `require("ajv")` from scripts/lib/install-state.js; the file
   already carries a complete hand-rolled validator enforcing the same
   schemas/install-state.schema.json (ecc.install.v1) constraints, so the
   installer closure is dependency-free (zero non-builtin bare requires).

Refs affaan-m/ECC#2502

* fix: avoid unpinned welcome invocations

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: validate translated skill frontmatter

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: repair skill frontmatter YAML

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>

* fix: add MIT license to core skill manifests; pin verification-loop tsc invocation

* fix: preserve tsc/pyright exit status in verification-loop type-check (set -o pipefail)

* chore(deps): sync lockfiles with exact-pinned package.json

Regenerate package-lock.json and yarn.lock so the pinned dependency
specs are reflected in both lockfiles. npm ci and Yarn's --immutable
install now pass the sync check. The resolution tree is unchanged
(231 yarn resolutions, byte-identical set; zero npm transitive drift);
only the root descriptor strings move from ranges to the versions
already resolved in the committed lockfiles.

Addresses the Codex P1 on #2503.

---------

Signed-off-by: Samar Tomar <samar_tomar@hotmail.com>
Co-authored-by: Samarjeet Singh Tomar <samartomar@gmail.com>
2026-07-17 17:13:49 -04:00

301 lines
10 KiB
JavaScript

const fs = require('fs');
const path = require('path');
// Dependency-free, self-contained validation. The installer closure must not
// require any non-builtin package (enterprise supply-chain vetting: the vetted
// bytes must be the installed bytes). install-state is validated by the
// hand-rolled validator below, which enforces the same constraints as
// schemas/install-state.schema.json (ecc.install.v1).
let cachedValidator = null;
function cloneJsonValue(value) {
if (value === undefined) {
return undefined;
}
return JSON.parse(JSON.stringify(value));
}
function readJson(filePath, label) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
throw new Error(`Failed to read ${label}: ${error.message}`);
}
}
function getValidator() {
if (cachedValidator) {
return cachedValidator;
}
cachedValidator = createFallbackValidator();
return cachedValidator;
}
function createFallbackValidator() {
const validate = state => {
const errors = [];
validate.errors = errors;
function pushError(instancePath, message) {
errors.push({
instancePath,
message,
});
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0;
}
function validateNoAdditionalProperties(value, instancePath, allowedKeys) {
for (const key of Object.keys(value)) {
if (!allowedKeys.includes(key)) {
pushError(`${instancePath}/${key}`, 'must NOT have additional properties');
}
}
}
function validateStringArray(value, instancePath) {
if (!Array.isArray(value)) {
pushError(instancePath, 'must be array');
return;
}
for (let index = 0; index < value.length; index += 1) {
if (!isNonEmptyString(value[index])) {
pushError(`${instancePath}/${index}`, 'must be non-empty string');
}
}
}
function validateOptionalString(value, instancePath) {
if (value !== undefined && value !== null && !isNonEmptyString(value)) {
pushError(instancePath, 'must be string or null');
}
}
if (!state || typeof state !== 'object' || Array.isArray(state)) {
pushError('/', 'must be object');
return false;
}
validateNoAdditionalProperties(
state,
'',
['schemaVersion', 'installedAt', 'lastValidatedAt', 'target', 'request', 'resolution', 'source', 'operations']
);
if (state.schemaVersion !== 'ecc.install.v1') {
pushError('/schemaVersion', 'must equal ecc.install.v1');
}
if (!isNonEmptyString(state.installedAt)) {
pushError('/installedAt', 'must be non-empty string');
}
if (state.lastValidatedAt !== undefined && !isNonEmptyString(state.lastValidatedAt)) {
pushError('/lastValidatedAt', 'must be non-empty string');
}
const target = state.target;
if (!target || typeof target !== 'object' || Array.isArray(target)) {
pushError('/target', 'must be object');
} else {
validateNoAdditionalProperties(target, '/target', ['id', 'target', 'kind', 'root', 'installStatePath']);
if (!isNonEmptyString(target.id)) {
pushError('/target/id', 'must be non-empty string');
}
validateOptionalString(target.target, '/target/target');
if (target.kind !== undefined && !['home', 'project'].includes(target.kind)) {
pushError('/target/kind', 'must be equal to one of the allowed values');
}
if (!isNonEmptyString(target.root)) {
pushError('/target/root', 'must be non-empty string');
}
if (!isNonEmptyString(target.installStatePath)) {
pushError('/target/installStatePath', 'must be non-empty string');
}
}
const request = state.request;
if (!request || typeof request !== 'object' || Array.isArray(request)) {
pushError('/request', 'must be object');
} else {
validateNoAdditionalProperties(
request,
'/request',
['profile', 'modules', 'includeComponents', 'excludeComponents', 'legacyLanguages', 'legacyMode']
);
if (!(Object.prototype.hasOwnProperty.call(request, 'profile') && (request.profile === null || typeof request.profile === 'string'))) {
pushError('/request/profile', 'must be string or null');
}
validateStringArray(request.modules, '/request/modules');
validateStringArray(request.includeComponents, '/request/includeComponents');
validateStringArray(request.excludeComponents, '/request/excludeComponents');
validateStringArray(request.legacyLanguages, '/request/legacyLanguages');
if (typeof request.legacyMode !== 'boolean') {
pushError('/request/legacyMode', 'must be boolean');
}
}
const resolution = state.resolution;
if (!resolution || typeof resolution !== 'object' || Array.isArray(resolution)) {
pushError('/resolution', 'must be object');
} else {
validateNoAdditionalProperties(resolution, '/resolution', ['selectedModules', 'skippedModules']);
validateStringArray(resolution.selectedModules, '/resolution/selectedModules');
validateStringArray(resolution.skippedModules, '/resolution/skippedModules');
}
const source = state.source;
if (!source || typeof source !== 'object' || Array.isArray(source)) {
pushError('/source', 'must be object');
} else {
validateNoAdditionalProperties(source, '/source', ['repoVersion', 'repoCommit', 'manifestVersion']);
validateOptionalString(source.repoVersion, '/source/repoVersion');
validateOptionalString(source.repoCommit, '/source/repoCommit');
if (!Number.isInteger(source.manifestVersion) || source.manifestVersion < 1) {
pushError('/source/manifestVersion', 'must be integer >= 1');
}
}
if (!Array.isArray(state.operations)) {
pushError('/operations', 'must be array');
} else {
for (let index = 0; index < state.operations.length; index += 1) {
const operation = state.operations[index];
const instancePath = `/operations/${index}`;
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
pushError(instancePath, 'must be object');
continue;
}
if (!isNonEmptyString(operation.kind)) {
pushError(`${instancePath}/kind`, 'must be non-empty string');
}
if (!isNonEmptyString(operation.moduleId)) {
pushError(`${instancePath}/moduleId`, 'must be non-empty string');
}
if (!isNonEmptyString(operation.sourceRelativePath)) {
pushError(`${instancePath}/sourceRelativePath`, 'must be non-empty string');
}
if (!isNonEmptyString(operation.destinationPath)) {
pushError(`${instancePath}/destinationPath`, 'must be non-empty string');
}
if (!isNonEmptyString(operation.strategy)) {
pushError(`${instancePath}/strategy`, 'must be non-empty string');
}
if (!isNonEmptyString(operation.ownership)) {
pushError(`${instancePath}/ownership`, 'must be non-empty string');
}
if (typeof operation.scaffoldOnly !== 'boolean') {
pushError(`${instancePath}/scaffoldOnly`, 'must be boolean');
}
}
}
return errors.length === 0;
};
validate.errors = [];
return validate;
}
function formatValidationErrors(errors = []) {
return errors
.map(error => `${error.instancePath || '/'} ${error.message}`)
.join('; ');
}
function validateInstallState(state) {
const validator = getValidator();
const valid = validator(state);
return {
valid,
errors: validator.errors || [],
};
}
function assertValidInstallState(state, label) {
const result = validateInstallState(state);
if (!result.valid) {
throw new Error(`Invalid install-state${label ? ` (${label})` : ''}: ${formatValidationErrors(result.errors)}`);
}
}
function createInstallState(options) {
const installedAt = options.installedAt || new Date().toISOString();
const state = {
schemaVersion: 'ecc.install.v1',
installedAt,
target: {
id: options.adapter.id,
target: options.adapter.target || undefined,
kind: options.adapter.kind || undefined,
root: options.targetRoot,
installStatePath: options.installStatePath,
},
request: {
profile: options.request.profile || null,
modules: Array.isArray(options.request.modules) ? [...options.request.modules] : [],
includeComponents: Array.isArray(options.request.includeComponents)
? [...options.request.includeComponents]
: [],
excludeComponents: Array.isArray(options.request.excludeComponents)
? [...options.request.excludeComponents]
: [],
legacyLanguages: Array.isArray(options.request.legacyLanguages)
? [...options.request.legacyLanguages]
: [],
legacyMode: Boolean(options.request.legacyMode),
},
resolution: {
selectedModules: Array.isArray(options.resolution.selectedModules)
? [...options.resolution.selectedModules]
: [],
skippedModules: Array.isArray(options.resolution.skippedModules)
? [...options.resolution.skippedModules]
: [],
},
source: {
repoVersion: options.source.repoVersion || null,
repoCommit: options.source.repoCommit || null,
manifestVersion: options.source.manifestVersion,
},
operations: Array.isArray(options.operations)
? options.operations.map(operation => cloneJsonValue(operation))
: [],
};
if (options.lastValidatedAt) {
state.lastValidatedAt = options.lastValidatedAt;
}
assertValidInstallState(state, 'create');
return state;
}
function readInstallState(filePath) {
const state = readJson(filePath, 'install-state');
assertValidInstallState(state, filePath);
return state;
}
function writeInstallState(filePath, state) {
assertValidInstallState(state, filePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`);
return state;
}
module.exports = {
createInstallState,
readInstallState,
validateInstallState,
writeInstallState,
};