@entrorove/skill 0.1.22 → 0.1.24

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.
Files changed (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +10 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +18 -83
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +12 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,56 @@
1
+ import { array, evidenceArray, integer, ledgerIdentifier as id, nullable, object, paths,
2
+ record, relativePath, sha256, text } from './local-contract-types.mjs';
3
+
4
+ const exact = properties => object(properties, Object.keys(properties));
5
+ const bounded = (items, maximum = 512) => ({ ...array(items, 1), maxItems: maximum });
6
+ const reference = exact({ kind: { const: 'snapshot' }, artifactId: id, manifestDigest: sha256 });
7
+ const stored = exact({ path: relativePath, exists: { type: 'boolean' }, sha256, mode: nullable(integer(0, 0o777)) });
8
+ const source = exact({ path: relativePath, sourcePath: relativePath, sha256, mode: integer(0, 0o777) });
9
+ const native = exact({ path: relativePath, sha256 });
10
+ const mappings = bounded(exact({ sourcePath: relativePath, targetPath: relativePath }));
11
+ const mappingProof = exact({ schemaVersion: { const: 'entrorove.merge-artifact-mapping/1.0' }, flowId: id,
12
+ ownerId: id, taskDigest: sha256, chainDigest: sha256, planId: id, planDigest: sha256,
13
+ blueprintBinding: record, scopeArtifactDigest: sha256,
14
+ entries: bounded(exact({ sourcePath: relativePath, targetPath: relativePath, sha256, producerNodeRef: id, producerStepId: id, stepId: id })) });
15
+ const signature = { issuedAt: text, expiresAt: text, nonce: id, keyId: sha256, signature: { ...text, pattern: '^[A-Za-z0-9_-]{86}$' } };
16
+ const identity = { mergeId: id, taskId: id, flowId: id, stepId: id, ownerId: id, taskDigest: sha256, chainDigest: sha256,
17
+ leaseId: id, generation: integer(1), epoch: integer(0), repositoryDigest: { ...text, pattern: '^[0-9a-f]{32}$' } };
18
+ const lease = { ...identity, chainRevision: integer(1), planId: id, planDigest: sha256, runnerId: id, runnerKeyId: sha256,
19
+ repositoryId: id, scopeBindingDigest: sha256, scopeArtifactDigest: sha256, scopeContractDigest: sha256,
20
+ scopeContractArtifactDigest: sha256, architectureContractDigest: sha256, architectureLedgerDigest: sha256,
21
+ baselineRef: text, paths, scopePaths: paths, mappings, mappingProof, mappingDigest: sha256, ...signature };
22
+ export const MERGE_LEASE_SCHEMA = exact({ schemaVersion: { const: 'entrorove.merge-lease/1.0' }, ...lease });
23
+ const artifacts = { sourceRef: reference, targetRef: reference, sourceFiles: bounded(source), targetFiles: bounded(stored),
24
+ afterManifest: bounded(native), beforeFullManifest: bounded(stored), mergedArtifactSha256: sha256,
25
+ snapshotDigest: sha256, snapshotReceiptDigest: sha256, mergeLeaseDigest: sha256 };
26
+ export const MERGE_PREPARATION_SCHEMA = exact({ schemaVersion: { const: 'entrorove.merge-preparation-receipt/1.0' },
27
+ ...identity, baselineRef: text, paths, scopePaths: paths, mappings, mappingProof, mappingDigest: sha256,
28
+ architectureContractDigest: sha256, architectureLedgerDigest: sha256, ...artifacts, ...signature });
29
+ export const MERGE_CONTRACT_SCHEMA = exact({ schemaVersion: { const: 'entrorove.merge-contract/1.0' }, ...lease,
30
+ ...artifacts, contractId: id, preparationDigest: sha256, archguardPreflightDigest: sha256,
31
+ evidenceDigests: bounded(sha256, 128), evidenceBindings: bounded(exact({ evidenceDigest: sha256, subjectDigest: sha256,
32
+ flowId: id, stepId: id, snapshotDigest: sha256, contractDigest: sha256, artifactSha256: sha256 }), 128) });
33
+ export const MERGE_ROLLBACK_AUTHORIZATION_SCHEMA = exact({ schemaVersion: { const: 'entrorove.merge-rollback-authorization/1.0' }, ...lease,
34
+ transactionId: id, originalStepId: id, originalLeaseId: id, originalGeneration: integer(1), originalContractDigest: sha256,
35
+ originalResultDigest: sha256, expectedJournalDigest: sha256, headReceiptDigest: sha256, contractRawDigest: sha256,
36
+ nativeLedgerHeadDigest: sha256, sourceRef: reference, targetRef: reference, beforeFiles: bounded(stored), restoreFiles: bounded(stored) });
37
+ const base = { repositoryRoot: text, home: text, flowId: id, stepId: id, taskId: id };
38
+ const actual = { ...base, runnerSignerConfigPath: text, mergeLease: MERGE_LEASE_SCHEMA, preparation: MERGE_PREPARATION_SCHEMA };
39
+ const freeze = object({ ...base, sourceRoot: text, snapshotReceipt: record, mergeLease: MERGE_LEASE_SCHEMA,
40
+ runnerSignerConfigPath: text }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotReceipt', 'mergeLease', 'runnerSignerConfigPath']);
41
+ const preflight = object(actual, ['repositoryRoot', 'flowId', 'stepId', 'taskId', 'mergeLease', 'preparation']);
42
+ const verified = object({ ...actual, contract: MERGE_CONTRACT_SCHEMA, sourceRef: reference, targetRef: reference,
43
+ preflight: record, evidence: evidenceArray, gatePassPath: text, executionAttemptNonce: id,
44
+ generation: integer(1), leaseId: id, workerId: id,
45
+ changes: bounded(object({ targetPath: relativePath, gatePassPath: text, content: {}, expectedSha256: nullable(sha256) }, ['targetPath'])) },
46
+ ['repositoryRoot', 'flowId', 'stepId', 'taskId', 'runnerSignerConfigPath', 'mergeLease', 'preparation', 'contract', 'preflight', 'evidence', 'gatePassPath', 'changes']);
47
+ const rollback = { oneOf: [
48
+ object({ ...base, transactionId: id, rollbackAuthorization: MERGE_ROLLBACK_AUTHORIZATION_SCHEMA, gatePassPath: text, runnerSignerConfigPath: text,
49
+ executionAttemptNonce: id, generation: integer(1), leaseId: id },
50
+ ['repositoryRoot', 'flowId', 'stepId', 'transactionId', 'rollbackAuthorization', 'gatePassPath', 'runnerSignerConfigPath']),
51
+ object({ ...base, snapshotId: id, gatePassPath: text, runnerSignerConfigPath: text, executionAttemptNonce: id,
52
+ generation: integer(1), leaseId: id }, ['repositoryRoot', 'flowId', 'snapshotId', 'gatePassPath', 'runnerSignerConfigPath']),
53
+ ] };
54
+ export const MERGE_LOCAL_OPERATION_SCHEMAS = Object.freeze({ 'merge-freeze': freeze, 'merge-preflight': preflight,
55
+ 'merge-verified': verified, rollback, 'merge-journal-head': object({ ...base, transactionId: id, runnerSignerConfigPath: text },
56
+ ['repositoryRoot', 'flowId', 'stepId', 'transactionId', 'runnerSignerConfigPath']) });
@@ -0,0 +1,170 @@
1
+ import path from 'node:path';
2
+ import { verifyTestEvidence } from './local-evidence-verification.mjs';
3
+ import { inspectMergePreparation, inspectMergeArchguardPreflight, mergePreflightBinding } from './local-merge-archguard-verification.mjs';
4
+ import { canonical, sha256, context, withMergeFence, fail, read } from './local-merge-storage.mjs';
5
+ import { inspectMergeContract, inspectMergeLease, inspectSigned, match, signMergeReceipt } from './local-merge-authority.mjs';
6
+ import { readFrozenMergeArtifact } from './local-merge-artifacts.mjs';
7
+ import { applyFileBatch, assertBatchImage, metadata } from './local-merge-transaction.mjs';
8
+ import { readMergeJournal, saveMergeJournal, appendMergeEvent, reserveMergeNonces, mergeNonceKeys, verifyPersistentMergeSignature,
9
+ assertNoMergeFailure, recordMergeFailure } from './local-merge-journal.mjs';
10
+ import { requireMergeMutationBridge, requireMergeOwnershipBridge, requireMergeArchguardBridge, verifyMergeMutationPass, verifyMergeMutationReceipt } from './local-merge-mutation.mjs';
11
+ import { inspectStoredMergeSuccess } from './local-merge-head.mjs';
12
+ export { rollbackMerge } from './local-merge-rollback.mjs';
13
+
14
+ const SCHEMA = 'entrorove.local-execution/1.0';
15
+ function reject(error) { return { schemaVersion: SCHEMA, status: 'blocked', merged: false, code: error.code ?? 'MERGE_BLOCKED', reason: error.message }; }
16
+ function requireVerified(value, label) { if (!value.verified) fail('MERGE_UNVERIFIED', `${label}: ${value.reason ?? 'independent authority is required'}`); }
17
+ function validateEvidence(input, preparation, contract) {
18
+ if (!Array.isArray(input.evidence) || !input.evidence.length || input.evidence.length > 128) fail('MERGE_EVIDENCE_REQUIRED', 'Merge requires complete independently trusted execution evidence');
19
+ if (!Array.isArray(contract.evidenceBindings) || contract.evidenceBindings.length !== input.evidence.length) fail('MERGE_EVIDENCE_BINDING', 'Frozen contract must bind every original verified business receipt');
20
+ const covered = new Set();
21
+ const checks = input.evidence.map(evidence => {
22
+ const binding = contract.evidenceBindings.find(item => item.evidenceDigest === sha256(canonical(evidence)));
23
+ if (!binding || Object.keys(binding).sort().join('|') !== 'artifactSha256|contractDigest|evidenceDigest|flowId|snapshotDigest|stepId|subjectDigest') fail('MERGE_EVIDENCE_BINDING', 'Evidence differs from its original independently frozen business binding');
24
+ const checked = verifyTestEvidence(evidence, { publicKey: process.env.ENTROROVE_VALIDATOR_RECEIPT_PUBLIC_KEY, binding, requireBinding: true });
25
+ if (!checked.verified || !checked.successful) fail('MERGE_EVIDENCE_UNVERIFIED', 'Every merge receipt must independently prove successful actual execution');
26
+ for (const file of preparation.sourceFiles) {
27
+ const entry = preparation.mappingProof.entries.find(item => item.sourcePath === file.sourcePath && item.targetPath === file.path && item.sha256 === file.sha256);
28
+ if (entry?.stepId === binding.stepId && evidence.subject.files.some(actual => actual.path === file.sourcePath && actual.sha256 === file.sha256)) covered.add(file.path);
29
+ }
30
+ return checked;
31
+ });
32
+ if (new Set(input.evidence.map(item => item.evidenceId)).size !== checks.length) fail('MERGE_EVIDENCE_DUPLICATE', 'Merge evidence identities must be unique');
33
+ if (covered.size !== preparation.sourceFiles.length) fail('MERGE_EVIDENCE_COVERAGE', 'Original trusted business evidence does not cover every native-mapped actual source output');
34
+ return checks;
35
+ }
36
+ async function validateAuthority(ctx, input, state, registration, verifyTargets = true) {
37
+ inspectMergeLease(input.mergeLease, ctx, state, registration);
38
+ const prepared = inspectMergePreparation(input.preparation, {}, { publicKey: process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY });
39
+ requireVerified(prepared, 'Frozen actual preparation');
40
+ for (const field of ['sourceRef', 'targetRef']) {
41
+ if (input[field] !== undefined && canonical(input[field]) !== canonical(input.preparation[field])) fail('MERGE_REF_BINDING', 'Explicit merge reference differs from its actual frozen preparation');
42
+ }
43
+ match(input.preparation, input.mergeLease, ['mergeId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'mappings', 'mappingDigest']);
44
+ const preflight = inspectMergeArchguardPreflight(input.preflight, mergePreflightBinding(input.preparation),
45
+ { preparation: input.preparation, mergeLease: input.mergeLease });
46
+ requireVerified(preflight, 'Independent actual Archguard preflight');
47
+ const checked = inspectMergeContract(input.contract, ctx, input.preparation, input.preflight, input.evidence);
48
+ validateEvidence(input, input.preparation, input.contract);
49
+ match(input.contract, input.mergeLease, ['mergeId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'mappings', 'mappingDigest', 'mappingProof', 'scopePaths']);
50
+ const source = await readFrozenMergeArtifact(ctx.input, input.preparation.sourceRef), target = await readFrozenMergeArtifact(ctx.input, input.preparation.targetRef);
51
+ if (source.manifest.role !== 'source' || target.manifest.role !== 'target'
52
+ || canonical(source.manifest.files.map(({ exists, ...file }) => file)) !== canonical(input.preparation.sourceFiles)
53
+ || canonical(target.manifest.files) !== canonical(input.preparation.beforeFullManifest)
54
+ || canonical(target.manifest.files.filter(file => input.mergeLease.paths.includes(file.path))) !== canonical(input.preparation.targetFiles)) fail('MERGE_ARTIFACT_BINDING', 'Frozen merge source or target differs from its authority');
55
+ const changes = input.changes;
56
+ if (!Array.isArray(changes) || !changes.length || canonical(changes.map(change => change.targetPath).sort()) !== canonical(input.preparation.paths)) fail('MERGE_CHANGES_REQUIRED', 'Merge changes must exactly cover its nonempty actual frozen differences');
57
+ for (const change of changes) {
58
+ const file = source.files.find(item => item.path === change.targetPath);
59
+ if (change.gatePassPath !== undefined && change.gatePassPath !== input.gatePassPath) fail('MERGE_PASS_BINDING', 'Explicit path gate cannot replace its original atomic batch gate');
60
+ const baseline = target.files.find(item => item.path === change.targetPath);
61
+ if (change.expectedSha256 !== undefined && change.expectedSha256 !== (baseline.exists ? baseline.sha256 : null)) {
62
+ fail('MERGE_TARGET_CAS', 'Caller baseline must retain the actual frozen target, including its original absence');
63
+ }
64
+ if (change.content !== undefined && (!Buffer.from(typeof change.content === 'string' ? change.content : JSON.stringify(change.content)).equals(file.bytes))) fail('MERGE_CONTENT_BINDING', 'Caller content cannot replace frozen actual source bytes');
65
+ }
66
+ if (verifyTargets) await assertBatchImage(ctx.input, target.files);
67
+ const mutation = verifyTargets ? await verifyMergeMutationPass(ctx, input, input.preparation.paths, target.files) : null;
68
+ const afterWide = target.files.map(file => source.files.find(item => item.path === file.path) ?? file);
69
+ return { checked, source, target, afterWide, mutation };
70
+ }
71
+ async function commit(ctx, input, state, registration, images, assertCurrent) {
72
+ const bridge = await requireMergeMutationBridge(), ownershipGuard = await requireMergeOwnershipBridge();
73
+ const transactionId = input.contract.mergeId, contractDigest = sha256(canonical(input.contract));
74
+ let token = null, startedAt;
75
+ const binding = { ...resultBinding(input), transactionId, contractDigest, ...images.mutation };
76
+ const writes = input.preparation.sourceFiles.filter(file => input.preparation.paths.includes(file.path))
77
+ .map(file => ({ path: file.path, status: 'written', contentDigest: file.sha256 }));
78
+ let journal = { transactionId, operation: 'merge', status: 'pending', contractDigest, contract: input.contract,
79
+ preparation: input.preparation, mergeLease: input.mergeLease, before: images.target.files.map(metadata), after: images.afterWide.map(metadata),
80
+ touched: [], createdParents: [], receipt: null, mutationPass: images.mutation.pass,
81
+ nonceKeys: mergeNonceKeys(input.contract, input.preparation, input.preflight, input.evidence) };
82
+ const outcome = await ownershipGuard({ ...ctx.input, paths: input.preparation.paths }, images.mutation.pass, () => applyFileBatch(ctx.input, images.target.files, images.afterWide, {
83
+ prepare: async () => { startedAt = new Date().toISOString(); journal.startedAt = startedAt; await saveMergeJournal(ctx, journal);
84
+ await reserveMergeNonces(ctx, transactionId, journal.nonceKeys, contractDigest);
85
+ token = await bridge.beginMutationBatch14({ ...ctx.input, signerConfigPath: input.runnerSignerConfigPath }, {
86
+ mutationPass: images.mutation.pass, contract: input.contract, contractDigest, transactionId,
87
+ beforeFiles: journal.before.filter(file => input.preparation.paths.includes(file.path)),
88
+ afterFiles: journal.after.filter(file => input.preparation.paths.includes(file.path)) }); },
89
+ progress: async progress => { journal = { ...journal, ...progress }; await saveMergeJournal(ctx, journal); },
90
+ finish: async () => {
91
+ await validateAuthority(ctx, input, state, registration, false);
92
+ await assertCurrent();
93
+ const mutationBatchReceipt = verifyMergeMutationReceipt(await bridge.finishMutationBatch14(token), binding, writes, true,
94
+ { files: journal.after.filter(file => input.preparation.paths.includes(file.path)) });
95
+ await appendMergeEvent(ctx, transactionId, 'files-applied-reported', { contractDigest });
96
+ const receipt = await signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-result-receipt/1.0', operation: 'merge',
97
+ status: 'verified', transactionId, contractDigest, ...resultBinding(input), beforeFiles: journal.before, afterFiles: journal.after,
98
+ ...mutationReceiptFields(images.mutation), mutationBatchReceipt, startedAt,
99
+ rollbackOutcome: 'retained', recoveryRequired: false });
100
+ assertMergeDeadline(input, state, images.mutation, mutationBatchReceipt, receipt);
101
+ journal = await saveMergeJournal(ctx, { ...journal, status: 'verified', receipt });
102
+ await assertCurrent();
103
+ assertMergeDeadline(input, state, images.mutation, mutationBatchReceipt, receipt); return receipt;
104
+ },
105
+ failed: async failure => {
106
+ await recordMergeFailure(ctx, transactionId, contractDigest, failure); let receipt = null, proofFailure = null;
107
+ try {
108
+ if (!token) fail('MERGE_CONSUMPTION_UNRESOLVED', 'Begin did not return an original mutation token; consumption cannot be fabricated');
109
+ const afterFiles = await Promise.all(journal.before.map(async file => metadata({ path: file.path, ...await read(path.join(ctx.root, file.path), ctx.input, file.path, true) })));
110
+ const current = afterFiles.filter(file => input.preparation.paths.includes(file.path));
111
+ const mutationBatchReceipt = verifyMergeMutationReceipt(await bridge.failMutationBatch14(token, {
112
+ code: /^[A-Z][A-Z0-9_]{0,127}$/.test(failure.code) ? failure.code : 'MERGE_BATCH_FAILED', rollbackOutcome: failure.rollbackOutcome }), binding,
113
+ current.map(file => ({ path: file.path, status: 'failed', contentDigest: file.sha256 })), false, { files: current });
114
+ receipt = await signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-result-receipt/1.0', operation: 'merge', status: 'failed',
115
+ transactionId, contractDigest, ...resultBinding(input), beforeFiles: journal.before, afterFiles,
116
+ ...mutationReceiptFields(images.mutation), mutationBatchReceipt, startedAt, rollbackOutcome: failure.rollbackOutcome, recoveryRequired: failure.recoveryRequired });
117
+ } catch (error) { proofFailure = { code: error.code ?? 'MERGE_FAILURE_PROOF', reason: error.message }; }
118
+ journal = await saveMergeJournal(ctx, { ...journal, status: 'failed', failure, receipt, proofFailure });
119
+ await appendMergeEvent(ctx, transactionId, 'failed', { contractDigest, failure, proofFailure });
120
+ if (proofFailure) throw Object.assign(new Error(proofFailure.reason), { code: proofFailure.code });
121
+ },
122
+ }));
123
+ return outcome.applied ? { schemaVersion: SCHEMA, status: 'verified', merged: true, transactionId, changes: input.preparation.paths.length, receipt: outcome.result }
124
+ : { schemaVersion: SCHEMA, status: 'failed', merged: false, transactionId, receipt: journal.receipt, ...outcome };
125
+ }
126
+ function mutationReceiptFields(value) {
127
+ return Object.fromEntries(['mutationPassDigest', 'mutationSnapshotDigest', 'executionAuthorityDigest', 'executionAttemptNonce'].map(field => [field, value[field]]));
128
+ }
129
+ function assertMergeDeadline(input, state, mutation, proof, receipt) {
130
+ const attempt = state.executionAttempts.find(value => value.authority.leaseId === mutation.pass.leaseId
131
+ && value.authority.generation === mutation.pass.generation);
132
+ const authorities = [input.contract, input.mergeLease, input.preparation, input.preflight, mutation.pass, proof, receipt,
133
+ attempt?.authority, ...input.evidence.map(value => value.receipt)];
134
+ if (authorities.some(value => !value || !Number.isFinite(Date.parse(value.expiresAt)) || Date.parse(value.expiresAt) <= Date.now())) {
135
+ fail('MERGE_AUTHORITY_EXPIRED', 'Whole merge must finish inside every original live lease, execution, pass, preparation, preflight and evidence lifetime');
136
+ }
137
+ }
138
+ function resultBinding(input) {
139
+ const value = input.contract;
140
+ return Object.fromEntries(['mergeId', 'taskId', 'flowId', 'stepId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch',
141
+ 'repositoryDigest', 'sourceRef', 'targetRef', 'paths', 'mergedArtifactSha256', 'mappingDigest', 'afterManifest'].map(field => [field, value[field]]));
142
+ }
143
+ export async function mergeVerified(input) {
144
+ try {
145
+ const ctx = await context(input);
146
+ return await withMergeFence(ctx, async (state, registration) => {
147
+ if (input.contract?.mergeId) await assertNoMergeFailure(ctx, input.contract.mergeId);
148
+ const previous = input.contract?.mergeId ? await readMergeJournal(ctx, input.contract.mergeId, true) : null;
149
+ if (previous) {
150
+ if (previous.contractDigest !== sha256(canonical(input.contract))) fail('MERGE_TRANSACTION_CONFLICT', 'An existing transaction cannot change its original frozen contract');
151
+ if (previous.status !== 'verified') fail('MERGE_RECOVERY_REQUIRED', 'A pending or failed transaction requires explicit authorized recovery');
152
+ await inspectStoredMergeSuccess(ctx, previous, registration);
153
+ match(previous.contract, { ...registration, epoch: state.epoch }, ['ownerId', 'taskDigest', 'chainDigest', 'taskId', 'epoch']);
154
+ if (Date.parse(previous.contract.expiresAt) <= Date.now()) return { schemaVersion: SCHEMA, status: 'reported', merged: false, historicalResult: previous.receipt, code: 'MERGE_HISTORICAL_RESULT' };
155
+ const archguard = await requireMergeArchguardBridge();
156
+ return archguard(ctx.input, input.preflight, async assertCurrent => {
157
+ await validateAuthority(ctx, input, state, registration, false); await assertCurrent();
158
+ inspectSigned(previous.receipt, 'entrorove.merge-result-receipt/1.0', process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY);
159
+ inspectSigned(previous.receipt.mutationBatchReceipt, 'entrorove.mutation-batch-consumption-receipt/1.0', process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY);
160
+ return { schemaVersion: SCHEMA, status: 'verified', merged: true, transactionId: previous.transactionId, recoveredResult: true, receipt: previous.receipt };
161
+ });
162
+ }
163
+ const archguard = await requireMergeArchguardBridge();
164
+ return archguard(ctx.input, input.preflight, async assertCurrent => {
165
+ const images = await validateAuthority(ctx, input, state, registration);
166
+ return commit(ctx, input, state, registration, images, assertCurrent);
167
+ });
168
+ });
169
+ } catch (error) { return reject(error); }
170
+ }
@@ -0,0 +1,56 @@
1
+ import { canonical, sha256, context, withMergeFence, fail } from './local-merge-storage.mjs';
2
+ import { inspectSigned, match, signMergeReceipt } from './local-merge-authority.mjs';
3
+ import { readMergeJournal, assertNoMergeFailure, assertMergeTransactionLedger, verifyPersistentMergeSignature } from './local-merge-journal.mjs';
4
+ import { readFrozenMergeArtifact } from './local-merge-artifacts.mjs';
5
+ import { assertBatchImage, metadata } from './local-merge-transaction.mjs';
6
+ import { verifyMergeMutationReceipt, inspectMergeMutationHistory } from './local-merge-mutation.mjs';
7
+
8
+ export async function inspectStoredMergeSuccess(ctx, journal, registration) {
9
+ const contract = journal.contract, result = journal.receipt;
10
+ if (journal.status !== 'verified' || journal.operation !== 'merge' || !result || result.status !== 'verified'
11
+ || result.operation !== 'merge' || result.rollbackOutcome !== 'retained' || result.recoveryRequired) {
12
+ fail('MERGE_JOURNAL_NOT_SUCCESSFUL', 'Rollback head requires the original actual successful merge transaction');
13
+ }
14
+ inspectSigned(contract, 'entrorove.merge-contract/1.0', process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY, Date.parse(contract.issuedAt));
15
+ verifyPersistentMergeSignature(result, 'entrorove.merge-result-receipt/1.0', ctx.repo);
16
+ match(contract, registration, ['ownerId', 'taskId', 'taskDigest', 'chainDigest', 'flowId', 'runnerKeyId']);
17
+ match(result, { ...contract, transactionId: journal.transactionId, contractDigest: journal.contractDigest },
18
+ ['transactionId', 'contractDigest', 'flowId', 'stepId', 'ownerId', 'taskId', 'taskDigest', 'chainDigest',
19
+ 'sourceRef', 'targetRef', 'mappingDigest', 'afterManifest', 'paths']);
20
+ if (journal.contractDigest !== sha256(canonical(contract))) fail('MERGE_JOURNAL_BINDING', 'Original merge contract bytes changed');
21
+ const proof = result.mutationBatchReceipt;
22
+ const writes = contract.sourceFiles.filter(file => contract.paths.includes(file.path)).map(file => ({ path: file.path, status: 'written', contentDigest: file.sha256 }));
23
+ verifyMergeMutationReceipt(proof, result, writes, true,
24
+ { now: Date.parse(proof?.issuedAt), files: result.afterFiles.filter(file => contract.paths.includes(file.path)) });
25
+ await inspectMergeMutationHistory(ctx.input, journal.mutationPass, proof,
26
+ result.beforeFiles.filter(file => contract.paths.includes(file.path)), result.afterFiles.filter(file => contract.paths.includes(file.path)));
27
+ await assertNoMergeFailure(ctx, journal.transactionId); await assertMergeTransactionLedger(ctx, journal);
28
+ const originalInput = { ...ctx.input, stepId: contract.stepId };
29
+ const source = await readFrozenMergeArtifact(originalInput, contract.sourceRef), target = await readFrozenMergeArtifact(originalInput, contract.targetRef);
30
+ if (canonical(target.manifest.files) !== canonical(contract.beforeFullManifest)) fail('MERGE_JOURNAL_BINDING', 'Original wide target artifact changed');
31
+ const after = target.files.map(file => source.files.find(item => item.path === file.path) ?? file);
32
+ if (canonical(journal.before) !== canonical(target.files.map(metadata)) || canonical(journal.after) !== canonical(after.map(metadata))
33
+ || canonical(result.beforeFiles) !== canonical(journal.before) || canonical(result.afterFiles) !== canonical(journal.after)) {
34
+ fail('MERGE_JOURNAL_BINDING', 'Original actual full-batch result differs from its immutable bytes');
35
+ }
36
+ await assertBatchImage(ctx.input, after); return { source, target, after };
37
+ }
38
+ export async function produceMergeJournalHead(input) {
39
+ const ctx = await context(input);
40
+ return withMergeFence(ctx, async (state, registration) => {
41
+ const journal = await readMergeJournal(ctx, input.transactionId);
42
+ if (ctx.stepId !== journal.contract.stepId) fail('MERGE_JOURNAL_BINDING', 'Journal head locator must retain its original merge step');
43
+ const contract = journal.contract;
44
+ const native = await import('./local-merge-archguard.mjs');
45
+ if (typeof native.withMergeArchguardStateFence !== 'function') fail('MERGE_ARCH_CURRENT_UNAVAILABLE', 'Fresh journal head requires the actual architecture and native history fence');
46
+ return native.withMergeArchguardStateFence(ctx.input, { architectureContractDigest: contract.architectureContractDigest }, async (assertCurrent, current) => {
47
+ await inspectStoredMergeSuccess(ctx, journal, registration); await assertCurrent();
48
+ const head = await signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-journal-head-receipt/1.0', transactionId: journal.transactionId,
49
+ mergeId: contract.mergeId, flowId: ctx.flowId, stepId: contract.stepId, repositoryDigest: ctx.repo, epoch: state.epoch,
50
+ ...Object.fromEntries(['ownerId', 'taskId', 'taskDigest', 'chainDigest', 'sourceRef', 'targetRef', 'paths', 'scopePaths'].map(field => [field, contract[field]])),
51
+ contractDigest: journal.contractDigest, originalResultDigest: sha256(canonical(journal.receipt)), journalDigest: sha256(canonical(journal)),
52
+ beforeFiles: journal.before, afterFiles: journal.after, ...current });
53
+ await assertCurrent(); return head;
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import { verify } from 'node:crypto';
3
+ import { canonical, sha256, identifier, json, writeJson, fail } from './local-merge-storage.mjs';
4
+ import { pinnedKey, signMergeReceipt } from './local-merge-authority.mjs';
5
+
6
+ export function verifyPersistentMergeSignature(value, schema, repo) {
7
+ const { signature, ...payload } = value ?? {}, trusted = pinnedKey(process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY);
8
+ if (value?.schemaVersion !== schema || value.repositoryDigest !== repo || value.keyId !== trusted.keyId
9
+ || typeof signature !== 'string' || Buffer.from(signature, 'base64url').length !== 64
10
+ || Buffer.from(signature, 'base64url').toString('base64url') !== signature
11
+ || !verify(null, Buffer.from(canonical(payload)), trusted.key, Buffer.from(signature, 'base64url'))) {
12
+ fail('MERGE_JOURNAL_TAMPERED', 'Durable merge state lost its original independently pinned runner signature');
13
+ }
14
+ return value;
15
+ }
16
+ export const journalFile = (ctx, transactionId) => path.join(ctx.state, 'transactions', identifier(transactionId), 'journal.json');
17
+ const failureFile = (ctx, transactionId) => path.join(ctx.state, 'transactions', identifier(transactionId), 'failure.json');
18
+ export async function assertNoMergeFailure(ctx, transactionId) {
19
+ try { await json(failureFile(ctx, transactionId)); }
20
+ catch (error) { if (error.code === 'ENOENT') return; throw error; }
21
+ fail('MERGE_RECOVERY_REQUIRED', 'An actual failed transaction remains permanently recorded and cannot be replayed as a new green merge');
22
+ }
23
+ export async function recordMergeFailure(ctx, transactionId, contractDigest, failure) {
24
+ await writeJson(failureFile(ctx, transactionId), { schemaVersion: 'entrorove.merge-failure-marker/1.0', status: 'failed',
25
+ repositoryDigest: ctx.repo, flowId: ctx.flowId, stepId: ctx.stepId, transactionId, contractDigest, failure,
26
+ at: new Date().toISOString(), authority: 'blocking-local-audit' });
27
+ }
28
+ export async function readMergeJournal(ctx, transactionId, optional = false) {
29
+ try {
30
+ const value = verifyPersistentMergeSignature(await json(journalFile(ctx, transactionId)), 'entrorove.merge-journal/1.0', ctx.repo);
31
+ if (value.transactionId !== transactionId || value.flowId !== ctx.flowId || value.stepId !== value.contract?.stepId) fail('MERGE_JOURNAL_BINDING', 'Merge journal belongs to another actual task or original transaction step');
32
+ return value;
33
+ } catch (error) { if (optional && error.code === 'ENOENT') return null; throw error; }
34
+ }
35
+ export async function saveMergeJournal(ctx, value) {
36
+ const signed = await signMergeReceipt(ctx.input, { ...value, schemaVersion: 'entrorove.merge-journal/1.0',
37
+ repositoryDigest: ctx.repo, flowId: ctx.flowId, stepId: value.contract?.stepId ?? ctx.stepId, operationStepId: ctx.stepId });
38
+ await writeJson(journalFile(ctx, value.transactionId), signed); return signed;
39
+ }
40
+ export function mergeNonceKeys(contract, preparation, preflight, evidence) {
41
+ return [contract, preparation, preflight, ...evidence.map(item => item.receipt)].map(value => `${value.keyId}:${value.nonce}`).sort();
42
+ }
43
+ async function readLedger(ctx) {
44
+ const file = path.join(ctx.home, '.entrorove', 'merges', ctx.repo, 'ledger.json'); let state;
45
+ try { state = verifyPersistentMergeSignature(await json(file), 'entrorove.merge-local-ledger/1.0', ctx.repo); }
46
+ catch (error) { if (error.code !== 'ENOENT') throw error;
47
+ state = { schemaVersion: 'entrorove.merge-local-ledger/1.0', repositoryDigest: ctx.repo, entries: [] }; }
48
+ if (!Array.isArray(state.entries) || state.entries.length > 50000) fail('MERGE_LEDGER_INVALID', 'Merge ledger must be bounded and append-only');
49
+ for (const [index, entry] of state.entries.entries()) {
50
+ const { entryDigest, ...body } = entry;
51
+ if (entry.sequence !== index + 1 || entry.previousDigest !== (state.entries[index - 1]?.entryDigest ?? null)
52
+ || entryDigest !== sha256(canonical(body))) fail('MERGE_LEDGER_TAMPERED', 'Merge ledger hash chain changed');
53
+ }
54
+ return { file, state };
55
+ }
56
+ export async function assertMergeTransactionLedger(ctx, journal) {
57
+ const { state } = await readLedger(ctx), entries = state.entries.filter(entry => entry.transactionId === journal.transactionId);
58
+ const pending = entries.find(entry => entry.event === 'pending');
59
+ if (!pending || pending.details.contractDigest !== journal.contractDigest
60
+ || canonical(pending.details.nonceKeys) !== canonical(journal.nonceKeys)
61
+ || !entries.some(entry => entry.event === 'files-applied-reported') || entries.some(entry => entry.event === 'failed')) {
62
+ fail('MERGE_LEDGER_BINDING', 'A cached merge requires its original signed append-only consumption and actual file-report history');
63
+ }
64
+ }
65
+ export async function appendMergeEvent(ctx, transactionId, event, details = {}) {
66
+ const { file, state } = await readLedger(ctx), entries = state.entries;
67
+ const body = { sequence: entries.length + 1, previousDigest: entries.at(-1)?.entryDigest ?? null,
68
+ transactionId, flowId: ctx.flowId, stepId: ctx.stepId, event, details, at: new Date().toISOString() };
69
+ entries.push({ ...body, entryDigest: sha256(canonical(body)) });
70
+ await writeJson(file, await signMergeReceipt(ctx.input, { schemaVersion: state.schemaVersion, repositoryDigest: ctx.repo, entries }));
71
+ }
72
+ export async function reserveMergeNonces(ctx, transactionId, nonceKeys, contractDigest) {
73
+ if (new Set(nonceKeys).size !== nonceKeys.length || nonceKeys.some(key => !/^[0-9a-f]{64}:[A-Za-z0-9._:-]+$/.test(key))) fail('MERGE_REPLAY', 'Duplicate or unknown signed evidence nonce cannot authorize a merge');
74
+ const { state } = await readLedger(ctx);
75
+ if (state.entries.some(entry => entry.details.nonceKeys?.some(key => nonceKeys.includes(key)))) fail('MERGE_REPLAY', 'A merge authority or execution receipt nonce has already been consumed');
76
+ await appendMergeEvent(ctx, transactionId, 'pending', { contractDigest, nonceKeys });
77
+ }
@@ -0,0 +1,97 @@
1
+ import path from 'node:path';
2
+ import { canonical, sha256, fail, identifier, paths, MISSING_SHA256, noLinks } from './local-merge-storage.mjs';
3
+ import { match, inspectSigned } from './local-merge-authority.mjs';
4
+ import { metadata } from './local-merge-transaction.mjs';
5
+
6
+ export async function requireMergeMutationBridge() {
7
+ let bridge;
8
+ try { bridge = await import('./local-mutation-batch.mjs'); }
9
+ catch (error) {
10
+ if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error;
11
+ fail('MERGE_MUTATION_BATCH_UNAVAILABLE', 'Real merge requires the original mutation authority batch-consumption implementation');
12
+ }
13
+ for (const name of ['beginMutationBatch14', 'finishMutationBatch14', 'failMutationBatch14']) {
14
+ if (typeof bridge[name] !== 'function') fail('MERGE_MUTATION_BATCH_UNAVAILABLE', 'Mutation authority has no complete original batch-consumption interface');
15
+ }
16
+ return bridge;
17
+ }
18
+ export async function requireMergeOwnershipBridge() {
19
+ const bridge = await import('./local-swarm-mutation.mjs');
20
+ if (typeof bridge.withSwarmMutationBatchLease !== 'function') {
21
+ fail('MERGE_SWARM_BATCH_UNAVAILABLE', 'Real merge requires the original cross-flow ownership guard to protect every batch path under one physical ownership lock');
22
+ }
23
+ return bridge.withSwarmMutationBatchLease;
24
+ }
25
+ export async function requireMergeArchguardBridge() {
26
+ const bridge = await import('./local-merge-archguard.mjs');
27
+ if (typeof bridge.withMergeArchguardFence !== 'function') fail('MERGE_ARCH_CURRENT_UNAVAILABLE', 'Actual merge requires the original architecture contract and native ledger locks throughout its whole batch');
28
+ return bridge.withMergeArchguardFence;
29
+ }
30
+ export async function verifyMergeMutationPass(ctx, input, names, beforeFiles) {
31
+ const { verifyMutationPass } = await import('./local-execution.mjs');
32
+ const { snapshotAuthority14 } = await import('./local-mutation-snapshot.mjs');
33
+ let original;
34
+ for (const targetPath of names) {
35
+ const change = input.changes?.find(item => item.targetPath === targetPath);
36
+ const checked = await verifyMutationPass({ ...ctx.input, gatePassPath: change?.gatePassPath ?? input.gatePassPath, targetPath });
37
+ const pass = checked.pass;
38
+ if (original && canonical(pass) !== canonical(original)) fail('MERGE_PASS_BINDING', 'One atomic batch must use one original mutation pass');
39
+ match(pass, input.contract, ['taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'repositoryDigest', 'flowId', 'stepId']);
40
+ if (canonical(pass.paths) !== canonical(names) || pass.executionPlanSha256 !== sha256(canonical(input.contract))) {
41
+ fail('MERGE_PASS_BINDING', 'Actual mutation pass must bind the exact frozen merge contract and changed subset');
42
+ }
43
+ original = pass;
44
+ }
45
+ const snapshot = await snapshotAuthority14(ctx.input, ctx.root, original.snapshotReceipt);
46
+ if (canonical(snapshot.manifest.files) !== canonical(beforeFiles.filter(file => names.includes(file.path)).map(metadata))) {
47
+ fail('MERGE_PASS_BINDING', 'Actual mutation snapshot must retain the exact frozen before subset');
48
+ }
49
+ return { pass: original, mutationPassDigest: sha256(canonical(original)), mutationSnapshotDigest: original.snapshotDigest,
50
+ executionAuthorityDigest: original.executionAuthorityDigest, executionAttemptNonce: original.executionAttemptNonce };
51
+ }
52
+ export function verifyMergeMutationReceipt(receipt, binding, writes, successful, options = {}) {
53
+ inspectSigned(receipt, 'entrorove.mutation-batch-consumption-receipt/1.0', process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY, options.now ?? Date.now());
54
+ match(receipt, { ...binding, passDigest: binding.mutationPassDigest }, ['flowId', 'taskId', 'stepId', 'leaseId', 'generation',
55
+ 'passDigest', 'contractDigest', 'transactionId', 'executionAuthorityDigest', 'executionAttemptNonce', 'paths']);
56
+ const common = ['schemaVersion', 'status', 'consumed', 'batchId', 'flowId', 'taskId', 'stepId', 'leaseId', 'generation',
57
+ 'passDigest', 'contractDigest', 'transactionId', 'executionAuthorityDigest', 'executionAttemptNonce', 'paths', 'writes',
58
+ 'keyId', 'nonce', 'issuedAt', 'expiresAt', 'signature'];
59
+ const extra = successful ? ['afterFiles'] : ['currentFiles', 'code', ...(receipt.reportedRollbackOutcome === undefined ? [] : ['reportedRollbackOutcome'])];
60
+ const files = successful ? receipt.afterFiles : receipt.currentFiles, status = successful ? 'written' : 'failed';
61
+ if (receipt.consumed !== true || receipt.status !== status || Object.keys(receipt).sort().join('|') !== [...common, ...extra].sort().join('|')
62
+ || !Array.isArray(files) || canonical(paths(files.map(file => file?.path))) !== canonical(receipt.paths)
63
+ || files.some(file => !file || Object.keys(file).sort().join('|') !== 'exists|mode|path|sha256' || typeof file.exists !== 'boolean'
64
+ || !/^[0-9a-f]{64}$/.test(file.sha256 ?? '') || (file.exists ? !Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777
65
+ : file.sha256 !== MISSING_SHA256 || file.mode !== null))
66
+ || canonical(receipt.writes) !== canonical(files.map(file => ({ path: file.path, status, contentDigest: file.sha256 })))
67
+ || canonical(receipt.writes) !== canonical(writes) || !options.files || canonical(files) !== canonical(options.files)) {
68
+ fail('MERGE_MUTATION_CONSUMPTION', 'Original consumed batch proof must preserve every exact signed actual file and completed or failed write');
69
+ }
70
+ identifier(receipt.batchId);
71
+ if (!successful && (!/^[A-Z][A-Z0-9_]{0,127}$/.test(receipt.code ?? '') || receipt.reportedRollbackOutcome !== undefined
72
+ && !['retained', 'restored', 'restoration-failed'].includes(receipt.reportedRollbackOutcome))) {
73
+ fail('MERGE_MUTATION_CONSUMPTION', 'Original failed consumption requires its real explicit failure and recovery outcome');
74
+ }
75
+ return receipt;
76
+ }
77
+ export async function inspectMergeMutationHistory(input, originalPass, receipt, beforeFiles, afterFiles) {
78
+ const { mutationPass14 } = await import('./local-mutation-wire.mjs');
79
+ const { readMutationConsumption14 } = await import('./local-mutation-pass-store.mjs');
80
+ const pass = mutationPass14(originalPass, { live: false }), passDigest = sha256(canonical(pass));
81
+ match(pass, { ...receipt, executionPlanSha256: receipt.contractDigest }, ['flowId', 'taskId', 'stepId', 'leaseId', 'generation',
82
+ 'executionAuthorityDigest', 'executionAttemptNonce', 'paths', 'executionPlanSha256']);
83
+ if (passDigest !== receipt.passDigest) fail('MERGE_MUTATION_CONSUMPTION', 'Historical proof lost its original cloud-signed pass');
84
+ const file = path.join(process.env.ENTROROVE_TASK_HANDOFF_STATE_DIRECTORY, 'mutations', pass.repositoryDigest, pass.flowId, `${identifier(pass.passId)}.json`);
85
+ await noLinks(file);
86
+ const ledger = await readMutationConsumption14(pass, { consumptionPath: file, executionHome: input.home });
87
+ const beforeDigest = sha256(canonical(beforeFiles)), afterDigest = sha256(canonical(afterFiles));
88
+ const batchDigest = sha256(canonical({ passDigest, beforeDigest, afterDigest, batchId: receipt.batchId }));
89
+ for (const after of afterFiles) {
90
+ const entry = ledger.writes[after.path];
91
+ if (!entry || entry.status !== 'written' || entry.contentDigest !== after.sha256 || entry.batchId !== receipt.batchId
92
+ || entry.batchDigest !== batchDigest || entry.beforeDigest !== beforeDigest || entry.afterDigest !== afterDigest) {
93
+ fail('MERGE_MUTATION_CONSUMPTION', 'Historical result differs from its actual durable completed original batch consumption');
94
+ }
95
+ }
96
+ return ledger;
97
+ }
@@ -0,0 +1,126 @@
1
+ import path from 'node:path';
2
+ import { context, withMergeFence, canonical, sha256, fail, paths, read, identifier } from './local-merge-storage.mjs';
3
+ import { MERGE_LEASE_FIELDS, inspectSigned, match, signMergeReceipt } from './local-merge-authority.mjs';
4
+ import { readMergeJournal, saveMergeJournal, appendMergeEvent, reserveMergeNonces, recordMergeFailure } from './local-merge-journal.mjs';
5
+ import { inspectStoredMergeSuccess } from './local-merge-head.mjs';
6
+ import { rollbackFileBatch, metadata } from './local-merge-transaction.mjs';
7
+ import { atomicSnapshotRollback } from './local-merge-snapshot-rollback.mjs';
8
+ import { requireMergeMutationBridge, requireMergeOwnershipBridge, verifyMergeMutationPass, verifyMergeMutationReceipt } from './local-merge-mutation.mjs';
9
+
10
+ const SCHEMA = 'entrorove.local-execution/1.0';
11
+ const AUTH_SCHEMA = 'entrorove.merge-rollback-authorization/1.0';
12
+ const extraFields = ['transactionId', 'originalStepId', 'originalLeaseId', 'originalGeneration', 'originalContractDigest',
13
+ 'originalResultDigest', 'expectedJournalDigest', 'headReceiptDigest', 'contractRawDigest', 'nativeLedgerHeadDigest',
14
+ 'sourceRef', 'targetRef', 'beforeFiles', 'restoreFiles'];
15
+ function inspectRollbackAuthorization(ctx, auth, journal, state, registration) {
16
+ inspectSigned(auth, AUTH_SCHEMA, process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY);
17
+ if (Object.keys(auth).sort().join('|') !== [...MERGE_LEASE_FIELDS, ...extraFields].sort().join('|')) fail('MERGE_ROLLBACK_SCHEMA', 'Rollback requires its exact independently signed original transaction and fresh operation contract');
18
+ match(auth, registration, ['ownerId', 'taskDigest', 'chainDigest', 'taskId', 'flowId', 'runnerKeyId']);
19
+ match(auth, { repositoryDigest: ctx.repo, flowId: ctx.flowId, stepId: ctx.stepId, epoch: state.epoch }, ['repositoryDigest', 'flowId', 'stepId', 'epoch']);
20
+ match(auth, { ...journal.contract, transactionId: journal.transactionId, originalStepId: journal.contract.stepId,
21
+ originalLeaseId: journal.contract.leaseId, originalGeneration: journal.contract.generation, originalContractDigest: journal.contractDigest,
22
+ originalResultDigest: sha256(canonical(journal.receipt)), expectedJournalDigest: sha256(canonical(journal)),
23
+ beforeFiles: journal.after, restoreFiles: journal.before },
24
+ ['transactionId', 'mergeId', 'originalStepId', 'originalLeaseId', 'originalGeneration', 'originalContractDigest',
25
+ 'originalResultDigest', 'expectedJournalDigest', 'sourceRef', 'targetRef', 'paths', 'scopePaths', 'mappings', 'mappingProof',
26
+ 'mappingDigest', 'planId', 'planDigest', 'baselineRef', 'architectureContractDigest', 'beforeFiles', 'restoreFiles']);
27
+ if (auth.stepId === auth.originalStepId || auth.leaseId === auth.originalLeaseId || !Number.isSafeInteger(auth.generation)
28
+ || auth.generation < 1 || canonical(paths(auth.paths)) !== canonical(auth.paths)
29
+ || canonical(paths(auth.scopePaths)) !== canonical(auth.scopePaths)
30
+ || !['headReceiptDigest', 'contractRawDigest', 'nativeLedgerHeadDigest'].every(field => /^[0-9a-f]{64}$/.test(auth[field] ?? ''))) {
31
+ fail('MERGE_ROLLBACK_BINDING', 'Rollback must use its fresh independent node, lease, current native state and immutable original images');
32
+ }
33
+ for (const field of ['transactionId', 'mergeId', 'leaseId', 'originalLeaseId', 'originalStepId']) identifier(auth[field]);
34
+ return auth;
35
+ }
36
+ function resultBinding(auth) {
37
+ const afterManifest = auth.restoreFiles.filter(file => auth.paths.includes(file.path)).map(({ path: filePath, sha256: digest }) => ({ path: filePath, sha256: digest }));
38
+ return { ...Object.fromEntries(['mergeId', 'taskId', 'flowId', 'stepId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation',
39
+ 'epoch', 'repositoryDigest', 'sourceRef', 'targetRef', 'paths', 'mappingDigest', 'originalContractDigest'].map(field => [field, auth[field]])),
40
+ transactionId: auth.transactionId, contractDigest: sha256(canonical(auth)), rollbackAuthorizationDigest: sha256(canonical(auth)),
41
+ afterManifest, mergedArtifactSha256: sha256(`validator.file-manifest/1.0\n${afterManifest.map(file => `${file.path}\0${file.sha256}`).join('\n')}`) };
42
+ }
43
+ function mutationFields(value) {
44
+ return Object.fromEntries(['mutationPassDigest', 'mutationSnapshotDigest', 'executionAuthorityDigest', 'executionAttemptNonce'].map(field => [field, value[field]]));
45
+ }
46
+ function assertDeadline(auth, state, mutation, proof, receipt) {
47
+ inspectSigned(auth, AUTH_SCHEMA, process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY);
48
+ const attempt = state.executionAttempts.find(value => value.authority.leaseId === auth.leaseId && value.authority.generation === auth.generation);
49
+ if ([auth, mutation.pass, proof, receipt, attempt?.authority].some(value => !value || Date.parse(value.expiresAt) <= Date.now()
50
+ || !Number.isFinite(Date.parse(value.expiresAt)))) fail('MERGE_AUTHORITY_EXPIRED', 'Rollback must finish inside its actual fresh contract, execution, mutation and receipt lifetimes');
51
+ }
52
+ async function actualWide(ctx, names) {
53
+ return Promise.all(names.map(async filePath => metadata({ path: filePath, ...await read(path.join(ctx.root, filePath), ctx.input, filePath, true) })));
54
+ }
55
+ async function rollbackCommit(ctx, input, state, journal, images, assertCurrent) {
56
+ const auth = input.rollbackAuthorization, bridge = await requireMergeMutationBridge(), guard = await requireMergeOwnershipBridge();
57
+ const binding = { ...resultBinding(auth), ...images.mutation }, auditId = `rollback-${sha256(canonical(auth)).slice(0, 48)}`;
58
+ const before = images.after, after = images.target.files, selected = files => files.filter(file => auth.paths.includes(file.path)).map(metadata);
59
+ let token = null, startedAt, latest = journal;
60
+ let attempt = { status: 'pending', authorization: auth, authorizationDigest: binding.contractDigest, touched: [], createdParents: [], receipt: null };
61
+ const outcome = await guard({ ...ctx.input, paths: auth.paths }, images.mutation.pass, () => rollbackFileBatch(ctx.input, before, after, journal.createdParents, {
62
+ prepare: async () => {
63
+ startedAt = new Date().toISOString(); attempt.startedAt = startedAt;
64
+ await reserveMergeNonces(ctx, auditId, [`${auth.keyId}:${auth.nonce}`], binding.contractDigest);
65
+ latest = await saveMergeJournal(ctx, { ...journal, rollback: attempt });
66
+ token = await bridge.beginMutationBatch14({ ...ctx.input, signerConfigPath: input.runnerSignerConfigPath }, {
67
+ mutationPass: images.mutation.pass, contract: auth, contractDigest: binding.contractDigest, transactionId: auth.transactionId,
68
+ beforeFiles: selected(before), afterFiles: selected(after) });
69
+ },
70
+ progress: async progress => { attempt = { ...attempt, ...progress }; latest = await saveMergeJournal(ctx, { ...journal, rollback: attempt }); },
71
+ finish: async () => {
72
+ await assertCurrent(); inspectSigned(auth, AUTH_SCHEMA, process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY);
73
+ const proof = verifyMergeMutationReceipt(await bridge.finishMutationBatch14(token), binding,
74
+ selected(after).map(file => ({ path: file.path, status: 'written', contentDigest: file.sha256 })), true, { files: selected(after) });
75
+ await appendMergeEvent(ctx, auditId, 'rollback-files-restored-reported', { authorizationDigest: binding.contractDigest });
76
+ const receipt = await signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-result-receipt/1.0', operation: 'rollback', status: 'verified',
77
+ ...resultBinding(auth), ...mutationFields(images.mutation), mutationBatchReceipt: proof, startedAt,
78
+ beforeFiles: before.map(metadata), afterFiles: after.map(metadata), rollbackOutcome: 'restored', recoveryRequired: false });
79
+ assertDeadline(auth, state, images.mutation, proof, receipt);
80
+ attempt = { ...attempt, status: 'verified', receipt, mutationPass: images.mutation.pass };
81
+ latest = await saveMergeJournal(ctx, { ...journal, status: 'restored', rollback: attempt });
82
+ await assertCurrent(); assertDeadline(auth, state, images.mutation, proof, receipt); return receipt;
83
+ },
84
+ failed: async failure => {
85
+ await recordMergeFailure(ctx, auditId, binding.contractDigest, failure);
86
+ let receipt = null, proofFailure = null;
87
+ try {
88
+ if (!token) fail('MERGE_CONSUMPTION_UNRESOLVED', 'Rollback begin returned no original token; failure proof cannot be fabricated');
89
+ const afterFiles = await actualWide(ctx, auth.scopePaths), current = afterFiles.filter(file => auth.paths.includes(file.path));
90
+ const proof = verifyMergeMutationReceipt(await bridge.failMutationBatch14(token, { code: /^[A-Z][A-Z0-9_]{0,127}$/.test(failure.code) ? failure.code : 'MERGE_ROLLBACK_FAILED',
91
+ rollbackOutcome: failure.rollbackOutcome }), binding, current.map(file => ({ path: file.path, status: 'failed', contentDigest: file.sha256 })), false, { files: current });
92
+ receipt = await signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-result-receipt/1.0', operation: 'rollback', status: 'failed',
93
+ ...resultBinding(auth), ...mutationFields(images.mutation), mutationBatchReceipt: proof, startedAt,
94
+ beforeFiles: before.map(metadata), afterFiles, rollbackOutcome: failure.rollbackOutcome === 'restored' ? 'retained' : 'restoration-failed', recoveryRequired: failure.recoveryRequired });
95
+ } catch (error) { proofFailure = { code: error.code ?? 'MERGE_FAILURE_PROOF', reason: error.message }; }
96
+ attempt = { ...attempt, status: 'failed', failure, receipt, proofFailure };
97
+ latest = await saveMergeJournal(ctx, { ...journal, status: failure.recoveryRequired ? 'failed' : 'verified', rollback: attempt });
98
+ await appendMergeEvent(ctx, auditId, 'rollback-failed', { failure, proofFailure });
99
+ if (proofFailure) throw Object.assign(new Error(proofFailure.reason), { code: proofFailure.code });
100
+ },
101
+ }));
102
+ if (!outcome.applied && outcome.restoredParents) {
103
+ try { await saveMergeJournal(ctx, { ...latest, createdParents: outcome.restoredParents }); }
104
+ catch (error) { outcome.recoveryRequired = true;
105
+ outcome.journalAuditFailure = { code: error.code ?? 'MERGE_AUDIT_IO', reason: error.message }; }
106
+ }
107
+ return { schemaVersion: SCHEMA, ...outcome, status: outcome.applied ? 'verified' : 'failed', rolledBack: outcome.applied,
108
+ transactionId: auth.transactionId, receipt: outcome.applied ? outcome.result : attempt.receipt,
109
+ rollbackOutcome: outcome.applied ? 'restored' : outcome.rollbackOutcome === 'restored' ? 'retained' : 'restoration-failed' };
110
+ }
111
+ export async function rollbackMerge(input) {
112
+ try {
113
+ if (!input.transactionId) return await atomicSnapshotRollback(input);
114
+ const ctx = await context(input);
115
+ return await withMergeFence(ctx, async (state, registration) => {
116
+ const journal = await readMergeJournal(ctx, input.transactionId), auth = inspectRollbackAuthorization(ctx, input.rollbackAuthorization, journal, state, registration);
117
+ const native = await import('./local-merge-archguard.mjs');
118
+ if (typeof native.withMergeArchguardStateFence !== 'function') fail('MERGE_ARCH_CURRENT_UNAVAILABLE', 'Rollback requires the original actual native architecture state fence');
119
+ return native.withMergeArchguardStateFence(ctx.input, auth, async assertCurrent => {
120
+ const images = await inspectStoredMergeSuccess(ctx, journal, registration);
121
+ const mutation = await verifyMergeMutationPass(ctx, { ...input, contract: auth }, auth.paths, images.after);
122
+ return rollbackCommit(ctx, input, state, journal, { ...images, mutation }, assertCurrent);
123
+ });
124
+ });
125
+ } catch (error) { return { schemaVersion: SCHEMA, status: 'blocked', rolledBack: false, code: error.code ?? 'MERGE_ROLLBACK_BLOCKED', reason: error.message }; }
126
+ }