@entrorove/skill 0.1.64 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITY-MAP.md +23 -0
- package/CHANGELOG.md +20 -0
- package/DELIVERY-BOUNDARIES.md +2 -0
- package/DESIGN-GATES.md +160 -0
- package/REUSE-DISCIPLINE.md +17 -1
- package/SKILL.md +4 -2
- package/blueprint-criterion-runner.mjs +1 -1
- package/cli.mjs +11 -4
- package/design-browser-checks.mjs +86 -0
- package/design-browser-identity.mjs +13 -0
- package/design-browser-state.mjs +41 -0
- package/design-browser.mjs +181 -0
- package/design-context.mjs +34 -0
- package/design-contracts.d.mts +4 -0
- package/design-contracts.mjs +77 -0
- package/design-declaration.mjs +17 -0
- package/design-frameworks.mjs +4 -0
- package/design-gate.mjs +19 -0
- package/design-lint.mjs +115 -0
- package/design-local-contracts.mjs +15 -0
- package/design-operations.mjs +81 -0
- package/design-policy.d.mts +5 -0
- package/design-policy.mjs +137 -0
- package/design-release.mjs +25 -0
- package/design-research.mjs +99 -0
- package/design-runner.mjs +31 -0
- package/design-store.mjs +116 -0
- package/design-ui.mjs +24 -0
- package/install-identity.mjs +16 -0
- package/local-cli.mjs +3 -1
- package/local-contracts.mjs +2 -0
- package/local-discipline-actions.mjs +5 -0
- package/local-discipline-batch.mjs +12 -3
- package/local-discipline-contracts.mjs +15 -3
- package/local-discipline-gate.mjs +9 -2
- package/local-discipline-repository.mjs +12 -8
- package/local-error.mjs +18 -1
- package/local-execution.mjs +2 -1
- package/local-mutation-authority.mjs +2 -0
- package/local-runner-discipline.mjs +17 -3
- package/local-scope-zip.mjs +21 -0
- package/manifest.json +1 -1
- package/package-archive.mjs +54 -0
- package/package-replacement.mjs +48 -0
- package/package.json +34 -6
- package/release-state.json +51 -25
- package/release.mjs +3 -1
- package/releases.json +24 -0
- package/reuse-discipline-release.mjs +3 -1
- package/rollback-history.mjs +51 -0
- package/rollback-runner.mjs +77 -0
- package/version.mjs +20 -3
- package/workflow-local-runner.mjs +9 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { atomicMutationJson14, mutationFail14, mutationFile14, mutationPath14, mutationStateRoot14,
|
|
4
|
+
privateMutationDirectory14, readMutationJson14 } from './local-mutation-files.mjs';
|
|
5
|
+
|
|
6
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
7
|
+
async function location(input, root, target, digest) {
|
|
8
|
+
mutationPath14(target);
|
|
9
|
+
if (!SHA.test(digest)) mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback comparison requires exact file digests');
|
|
10
|
+
const state = await mutationStateRoot14(input), repositoryDigest = sha256(root);
|
|
11
|
+
const directory = path.join(state, 'rollback-baselines', repositoryDigest, sha256(target));
|
|
12
|
+
return { state, directory, file: path.join(directory, `${digest}.json`), repositoryDigest };
|
|
13
|
+
}
|
|
14
|
+
async function read(loc, target, digest) {
|
|
15
|
+
let info;
|
|
16
|
+
try { info = await mutationFile14(loc.file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
17
|
+
if (info.stat.size > 8192) mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback baseline record exceeds its bounded schema');
|
|
18
|
+
const record = await readMutationJson14(loc.file), { recordDigest, ...body } = record;
|
|
19
|
+
if (body.schemaVersion !== 'entrorove.rollback-baseline/1.0' || body.repositoryDigest !== loc.repositoryDigest
|
|
20
|
+
|| body.targetPath !== target || body.baselineDigest !== digest || sha256(canonical(body)) !== recordDigest
|
|
21
|
+
|| !['mutation-snapshot', 'approved-process-comparison'].includes(body.proof?.kind)
|
|
22
|
+
|| !SHA.test(body.proof.authorityDigest ?? '') || !SHA.test(body.proof.baselineManifestDigest ?? '')
|
|
23
|
+
|| body.claimsCompletedWrite !== false || body.grantsRollbackAuthority !== false) {
|
|
24
|
+
mutationFail14('ENTROROVE_ROLLBACK_HISTORY_INVALID', 'Rollback baseline record is invalid; preserve the record and original failure');
|
|
25
|
+
}
|
|
26
|
+
return record;
|
|
27
|
+
}
|
|
28
|
+
export async function assertRollbackCandidate(input, root, target, current, candidate) {
|
|
29
|
+
if (current === candidate) return;
|
|
30
|
+
const loc = await location(input, root, target, candidate);
|
|
31
|
+
if (await read(loc, target, candidate)) mutationFail14('ENTROROVE_ROLLBACK_PROOF_REQUIRED',
|
|
32
|
+
'Candidate restores an actually observed historical file image; use the original independently signed rollback with exact current/restore hashes and transaction records');
|
|
33
|
+
}
|
|
34
|
+
// These immutable observations prove only earlier bytes. They never claim a
|
|
35
|
+
// pending process finished, revive old write authority, or authorize rollback.
|
|
36
|
+
export async function rememberRollbackBaseline(input, root, target, digest, proof) {
|
|
37
|
+
const loc = await location(input, root, target, digest);
|
|
38
|
+
if (await read(loc, target, digest)) return;
|
|
39
|
+
const body = { schemaVersion: 'entrorove.rollback-baseline/1.0', repositoryDigest: loc.repositoryDigest,
|
|
40
|
+
targetPath: target, baselineDigest: digest, proof, claimsCompletedWrite: false, grantsRollbackAuthority: false };
|
|
41
|
+
await privateMutationDirectory14(loc.directory, { privateFrom: loc.state });
|
|
42
|
+
try { await atomicMutationJson14(loc.file, { ...body, recordDigest: sha256(canonical(body)) }, { privateFrom: loc.state }); }
|
|
43
|
+
catch (error) { if (error.code !== 'ENTROROVE_STATE_EXISTS' || !await read(loc, target, digest)) throw error; }
|
|
44
|
+
}
|
|
45
|
+
export async function rememberMutationBaselines(input, value, before) {
|
|
46
|
+
const proof = { kind: 'mutation-snapshot', authorityDigest: sha256(canonical(value.pass)),
|
|
47
|
+
baselineManifestDigest: value.pass.snapshotManifestDigest };
|
|
48
|
+
for (const file of before) if (file.exists) {
|
|
49
|
+
await rememberRollbackBaseline(input, value.binding.root, file.path, file.sha256, proof);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { mutationFail14, mutationRead14, mutationTarget14 } from './local-mutation-files.mjs';
|
|
4
|
+
import { archiveFormat } from './package-archive.mjs';
|
|
5
|
+
import { assertPackageReplacement } from './package-replacement.mjs';
|
|
6
|
+
import { assertRollbackCandidate, rememberRollbackBaseline } from './rollback-history.mjs';
|
|
7
|
+
|
|
8
|
+
const rollbackVerbs = new Set(['reset', 'restore', 'revert', 'checkout', 'switch']);
|
|
9
|
+
function gitOperation(args) {
|
|
10
|
+
for (let index = 0; index < args.length; index++) {
|
|
11
|
+
const value = args[index];
|
|
12
|
+
if (['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env'].includes(value)) { index++; continue; }
|
|
13
|
+
if (value.startsWith('-')) continue;
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
export function assertNoBlindRollback(policy) {
|
|
19
|
+
const executable = path.basename(policy.executable).replace(/\.exe$/i, '').toLowerCase(), args = policy.args ?? [];
|
|
20
|
+
const direct = executable === 'git' && rollbackVerbs.has(gitOperation(args));
|
|
21
|
+
const shell = ['sh', 'bash', 'zsh', 'dash', 'cmd', 'powershell', 'pwsh'].includes(executable);
|
|
22
|
+
const script = args.join(' ');
|
|
23
|
+
const indirect = shell && /(?:^|[;&|\n\s])(?:\S*\/)?git\s+(?:(?:-C|-c|--git-dir|--work-tree)\s+\S+\s+)*(?:reset|restore|revert|checkout|switch)(?:\s|$)/i.test(script);
|
|
24
|
+
if (direct || indirect) mutationFail14('ENTROROVE_ROLLBACK_PROOF_REQUIRED',
|
|
25
|
+
'Direct rollback commands cannot run through an ordinary validation plan; use the independently signed exact-image rollback entry, preserving original transaction and failure records');
|
|
26
|
+
if (shell && /(?:^|[;&|\n\s])(?:\S*\/)?(?:npm|pnpm|yarn|bun)\s+(?:i|install|add|update|upgrade|ci|exec|dlx)(?:\s|$)/i.test(script)) {
|
|
27
|
+
mutationFail14('ENTROROVE_PACKAGE_PROVENANCE_REQUIRED', 'Shell-wrapped package replacement cannot prove its destination or frozen inputs; use a direct frozen package-manager plan or the managed installer');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertFrozenInstall(authority) {
|
|
31
|
+
const policy = authority.plan.policy, executable = path.basename(policy.executable).replace(/\.(?:exe|cmd)$/i, '').toLowerCase();
|
|
32
|
+
const args = policy.args ?? [], operation = args.find(value => ['i', 'install', 'add', 'update', 'upgrade', 'ci', 'exec', 'dlx'].includes(value));
|
|
33
|
+
if (!['npm', 'pnpm', 'yarn', 'bun'].includes(executable) || !operation) return;
|
|
34
|
+
const install = ['install', 'i', 'ci'].includes(operation);
|
|
35
|
+
const frozen = executable === 'npm' ? operation === 'ci' : args.includes('--frozen-lockfile') || executable === 'yarn' && args.includes('--immutable');
|
|
36
|
+
const files = authority.plan.files ?? [], manifest = files.some(file => file.path === 'package.json');
|
|
37
|
+
const lock = files.some(file => (executable === 'npm' ? ['package-lock.json', 'npm-shrinkwrap.json']
|
|
38
|
+
: executable === 'pnpm' ? ['pnpm-lock.yaml'] : executable === 'yarn' ? ['yarn.lock'] : ['bun.lock', 'bun.lockb']).includes(file.path));
|
|
39
|
+
const positional = args.slice(args.indexOf(operation) + 1).filter((value, index, rest) => !value.startsWith('-') && !(index && rest[index - 1].startsWith('--')));
|
|
40
|
+
if (!install || !frozen || !manifest || !lock || positional.length || args.some(value => /^(?:--prefix|--dir|--cwd|--global|-g|--force)(?:=|$)/.test(value))) {
|
|
41
|
+
mutationFail14('ENTROROVE_PACKAGE_PROVENANCE_REQUIRED', 'Ordinary runner package installation requires its actual frozen manifest/lock in the original tested source set and no supplied package or destination override; use the managed installer for skill updates');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const packagePath = target => /(?:^|\/)(?:package|manifest)\.json$/i.test(target) || archiveFormat(target, Buffer.alloc(0));
|
|
45
|
+
export async function replacementRunnerBefore(input, authority) {
|
|
46
|
+
assertNoBlindRollback(authority.plan.policy);
|
|
47
|
+
assertFrozenInstall(authority);
|
|
48
|
+
const originals = new Map(), writes = authority.plan.policy.discipline?.writes ?? [];
|
|
49
|
+
for (const write of writes) {
|
|
50
|
+
const current = await mutationTarget14(authority.root, write.targetPath, true);
|
|
51
|
+
if (current.exists) {
|
|
52
|
+
if (current.stat.size > 64 * 1024 * 1024) {
|
|
53
|
+
if (packagePath(write.targetPath)) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Package baseline exceeds bounded inspection; no process was started');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const bytes = (await mutationRead14(input, authority.root, write.targetPath)).content;
|
|
57
|
+
if (packagePath(write.targetPath) || archiveFormat(write.targetPath, bytes)) originals.set(write.targetPath, bytes);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return originals;
|
|
61
|
+
}
|
|
62
|
+
export async function replacementRunnerAfter(input, authority, before, after, changed, originals) {
|
|
63
|
+
const proof = { kind: 'approved-process-comparison', authorityDigest: authority.planFile?.sha256 ?? sha256(canonical(authority.plan)),
|
|
64
|
+
baselineManifestDigest: sha256(canonical(before)) };
|
|
65
|
+
for (const target of changed) {
|
|
66
|
+
const previous = before.files.find(file => file.path === target), current = after.files.find(file => file.path === target);
|
|
67
|
+
if (!previous || !current || previous.kind !== 'file' || current.kind !== 'file') continue;
|
|
68
|
+
await assertRollbackCandidate(input, authority.root, target, previous.sha256, current.sha256);
|
|
69
|
+
if (previous.sha256 !== current.sha256 && (packagePath(target) || originals.has(target))) {
|
|
70
|
+
if (!originals.has(target)) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Process replaced an undeclared package without a verifiable original image; preserve this failed run and unknown effects');
|
|
71
|
+
const state = await mutationTarget14(authority.root, target);
|
|
72
|
+
if (state.stat.size > 64 * 1024 * 1024) mutationFail14('ENTROROVE_PACKAGE_VERSION_UNKNOWN', 'Replacement exceeds bounded package inspection');
|
|
73
|
+
assertPackageReplacement(target, originals.get(target), (await mutationRead14(input, authority.root, target)).content);
|
|
74
|
+
}
|
|
75
|
+
await rememberRollbackBaseline(input, authority.root, target, previous.sha256, proof);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/version.mjs
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
// SemVer 2 precedence, without Number rounding or lexical version ordering.
|
|
2
|
+
export function versionParts(value) {
|
|
3
|
+
const match = typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
|
|
4
|
+
if (!match || match[4]?.split('.').some(part => /^\d+$/.test(part) && part.length > 1 && part[0] === '0')) {
|
|
5
|
+
throw Object.assign(new Error('Package version must be an unambiguous SemVer value'), { code: 'ENTROROVE_PACKAGE_VERSION_UNKNOWN' });
|
|
6
|
+
}
|
|
7
|
+
return { core: match.slice(1, 4).map(BigInt), pre: match[4]?.split('.') ?? [] };
|
|
8
|
+
}
|
|
1
9
|
export function compareVersions(left, right) {
|
|
2
|
-
const a = left
|
|
3
|
-
const b = right.split('.').map(Number);
|
|
10
|
+
const a = versionParts(left), b = versionParts(right);
|
|
4
11
|
for (let index = 0; index < 3; index += 1) {
|
|
5
|
-
if (a[index] !== b[index]) return a[index]
|
|
12
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1;
|
|
13
|
+
}
|
|
14
|
+
if (!a.pre.length || !b.pre.length) return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1;
|
|
15
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
|
|
16
|
+
const x = a.pre[index], y = b.pre[index];
|
|
17
|
+
if (x === y) continue;
|
|
18
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
19
|
+
const numericX = /^\d+$/.test(x), numericY = /^\d+$/.test(y);
|
|
20
|
+
if (numericX && numericY) return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
21
|
+
if (numericX !== numericY) return numericX ? -1 : 1;
|
|
22
|
+
return x > y ? 1 : -1;
|
|
6
23
|
}
|
|
7
24
|
return 0;
|
|
8
25
|
}
|
|
@@ -312,6 +312,15 @@ export async function observeWorkflowLocalStep(rawInput) {
|
|
|
312
312
|
return { ...structuredClone(state.result), cached: true };
|
|
313
313
|
}));
|
|
314
314
|
}
|
|
315
|
+
export function verifyOriginalWorkflowPlan(value) {
|
|
316
|
+
const configured = publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY');
|
|
317
|
+
const plan = validatePlan(verifyAuthority(value, configured));
|
|
318
|
+
lifetime(plan.acceptedAt, plan.expiresAt, MAX_PLAN_MS, Date.now());
|
|
319
|
+
return plan;
|
|
320
|
+
}
|
|
321
|
+
export function verifyOriginalWorkflowAuthority(value) {
|
|
322
|
+
return verifyAuthority(value, publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY'));
|
|
323
|
+
}
|
|
315
324
|
export async function runWorkflowLocalStep(rawInput) {
|
|
316
325
|
let input; try { input = structuredClone(rawInput); } catch { fail('ENTROROVE_WORKFLOW_JSON', 'workflow values must be finite JSON'); }
|
|
317
326
|
const authority = await prepare(input);
|