@entrorove/skill 0.1.23 → 0.1.25
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/AGREEMENT.md +6 -6
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +18 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +35 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +24 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +65 -24
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +126 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-error.mjs +23 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +106 -0
- package/local-formula-engine.mjs +125 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +142 -0
- package/local-ledger.mjs +15 -9
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +103 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +89 -0
- package/local-mutation-files.mjs +270 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +41 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +164 -3
- package/release-state.json +164 -18
- package/release.mjs +242 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
package/local-ledger.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationSchemasFor } from './local-contracts.mjs';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { promises as fs } from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -18,9 +19,20 @@ async function repository(value) { if (typeof value !== 'string' || !path.isAbso
|
|
|
18
19
|
function repoDigest(root) { return sha(root).slice(0, 32); }
|
|
19
20
|
async function privateDir(directory) { await fs.mkdir(directory, { recursive: true, mode: 0o700 }); const stat = await fs.lstat(directory); if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'state directory must be a real directory'); if (process.platform !== 'win32') await fs.chmod(directory, 0o700); }
|
|
20
21
|
async function atomic(file, value) { await privateDir(path.dirname(file)); const tmp = `${file}.${randomUUID()}.tmp`; await fs.writeFile(tmp, `${canonical(value)}\n`, { flag: 'wx', mode: 0o600 }); await fs.rename(tmp, file); if (process.platform !== 'win32') await fs.chmod(file, 0o600); }
|
|
21
|
-
async function read(file) { await privateDir(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'ledger state must be a regular file'); return JSON.parse(await fs.readFile(file, 'utf8')); }
|
|
22
|
+
async function read(file, ensureDirectory = true) { if (ensureDirectory) await privateDir(path.dirname(file)); const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) fail('ENTROROVE_STATE_INVALID', 'ledger state must be a regular file'); return JSON.parse(await fs.readFile(file, 'utf8')); }
|
|
22
23
|
function ledgerPath(home, root) { return path.join(rootFor(home), 'ledgers', repoDigest(root), 'state.json'); }
|
|
23
24
|
async function loadLedger(input, create = false) { const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root); try { const state = await read(file); if (state.schemaVersion !== LEDGER_SCHEMA || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_LEDGER_INVALID', 'ledger identity mismatch'); return { root, file, state }; } catch (error) { if (!create || error.code !== 'ENOENT') throw error; const state = { schemaVersion: LEDGER_SCHEMA, repositoryDigest: repoDigest(root), sequence: 0, head: null, activeBranch: null, branches: [], records: [] }; await atomic(file, state); return { root, file, state }; } }
|
|
25
|
+
async function readLedgerForQuery(input) {
|
|
26
|
+
const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root);
|
|
27
|
+
try {
|
|
28
|
+
const state = await read(file, false);
|
|
29
|
+
if (state.schemaVersion !== LEDGER_SCHEMA || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_LEDGER_INVALID', 'ledger identity mismatch');
|
|
30
|
+
return { root, file, state };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error;
|
|
33
|
+
return { root, file, state: { schemaVersion: LEDGER_SCHEMA, repositoryDigest: repoDigest(root), sequence: 0, head: null, activeBranch: null, branches: [], records: [] } };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
async function append(input, event, data = {}) { const root = await repository(input.repositoryRoot); const file = ledgerPath(input.home, root); const previous = ledgerLocks.get(file) ?? Promise.resolve(); let release; const current = new Promise(resolve => { release = resolve; }); const queued = previous.then(() => current); ledgerLocks.set(file, queued); await previous; try { return await withStateLock(file, async () => { const authority = await loadLedger(input, true); const at = new Date().toISOString(); const unsigned = { sequence: authority.state.sequence + 1, event, at, ...data }; const record = { ...unsigned, previousDigest: authority.state.head, digest: sha(canonical(unsigned) + (authority.state.head ?? '')) }; const state = { ...authority.state, sequence: record.sequence, head: record.digest, records: [...authority.state.records, record] }; await atomic(authority.file, state); return { authority, state, record }; }); } finally { release(); if (ledgerLocks.get(file) === queued) ledgerLocks.delete(file); } }
|
|
25
37
|
export async function appendLedgerRecord(input, event, data = {}) { if (typeof event !== 'string' || !event.trim()) fail('ENTROROVE_LEDGER_EVENT', 'ledger event is required'); return (await append(input, event, data)).record; }
|
|
26
38
|
export async function branchCreate(input) { const name = id(input.name ?? input.branchName, 'branchName'); const fromRef = typeof input.fromRef === 'string' && input.fromRef ? input.fromRef : 'working-tree'; const authority = await loadLedger(input, true); if (authority.state.branches.some(item => item.name === name)) fail('ENTROROVE_BRANCH_EXISTS', `virtual branch already exists: ${name}`); const branch = { branchId: `branch-${randomUUID()}`, name, fromRef, description: input.description ?? '', virtual: true, createdAt: new Date().toISOString(), snapshotId: input.snapshotId ?? null }; const state = { ...authority.state, branches: [...authority.state.branches, branch], activeBranch: authority.state.activeBranch ?? name }; await atomic(authority.file, state); await append(input, 'branch-create', { name, fromRef, virtual: true }); return { schemaVersion: LEDGER_SCHEMA, status: 'verified', branch, forbidGitBranch: true, forbidWorktree: true }; }
|
|
@@ -35,7 +47,7 @@ export async function preflight(input) {
|
|
|
35
47
|
const reasons = []; if (!archguard.verified) reasons.push(archguard.reason); if (!evidence) reasons.push('trusted signed evidence bound to this flow, snapshot, and contract with exitCode 0 is required');
|
|
36
48
|
return { schemaVersion: LEDGER_SCHEMA, status: archguard.verified && evidence ? 'verified' : 'blocked', source: 'entrorove-ledger', checks: { archguard: archguard.verified, evidence }, reasons, virtualOnly: true, keyId: archguard.keyId ?? null };
|
|
37
49
|
}
|
|
38
|
-
export async function ledgerQuery(input) { const authority = await
|
|
50
|
+
export async function ledgerQuery(input) { const authority = await readLedgerForQuery(input); const limit = Number.isSafeInteger(input.limit) ? Math.min(500, Math.max(1, input.limit)) : 100; return { schemaVersion: LEDGER_SCHEMA, status: 'verified', repositoryDigest: repoDigest(authority.root), records: authority.state.records.slice(-limit), head: authority.state.head, sequence: authority.state.sequence }; }
|
|
39
51
|
|
|
40
52
|
function validatePlan(plan) { if (!plan || typeof plan !== 'object' || Array.isArray(plan) || plan.schemaVersion !== 'entrorove.execution-plan/1.0' || !ID.test(plan.chainId) || !Array.isArray(plan.steps) || !plan.steps.length) fail('ENTROROVE_CHAIN_PLAN_INVALID', 'chain plan must declare a schema, chainId, and steps'); const seen = new Set(); for (const [index, step] of plan.steps.entries()) { if (!step || typeof step !== 'object' || !ID.test(step.stepId) || seen.has(step.stepId) || !['command', 'skill', 'coordinator', 'decision'].includes(step.kind)) fail('ENTROROVE_CHAIN_STEP_INVALID', `step ${index} is invalid`); seen.add(step.stepId); } return plan; }
|
|
41
53
|
async function chainAuthority(input, create = false) { const root = await repository(input.repositoryRoot); const chainId = id(input.chainId ?? input.plan?.chainId, 'chainId'); const file = path.join(rootFor(input.home), 'chains', repoDigest(root), chainId, 'state.json'); try { const state = await read(file); if (state.schemaVersion !== CHAIN_SCHEMA || state.chainId !== chainId || state.repositoryDigest !== repoDigest(root)) fail('ENTROROVE_CHAIN_INVALID', 'chain state identity mismatch'); return { root, chainId, file, state }; } catch (error) { if (!create || error.code !== 'ENOENT') throw error; return { root, chainId, file, state: null }; } }
|
|
@@ -49,10 +61,4 @@ export const LOCAL_LEDGER_OPERATIONS = Object.freeze({
|
|
|
49
61
|
'chain-init': chainInit, 'chain-resume': chainResume, 'chain-status': chainStatus, 'chain-answer': chainAnswer,
|
|
50
62
|
'branch-create-virtual': branchCreate, 'chain-ledger-init': chainInit, 'chain-ledger-status': chainStatus, 'chain-ledger-query': ledgerQuery,
|
|
51
63
|
});
|
|
52
|
-
const
|
|
53
|
-
'branch-create': ['repositoryRoot', 'name', 'fromRef'], 'branch-list': ['repositoryRoot'], 'branch-switch': ['repositoryRoot', 'branchName'],
|
|
54
|
-
'diff-report': ['repositoryRoot'], preflight: ['repositoryRoot'], 'ledger-query': ['repositoryRoot'], 'chain-init': ['repositoryRoot', 'plan'],
|
|
55
|
-
'chain-resume': ['repositoryRoot', 'chainId'], 'chain-status': ['repositoryRoot', 'chainId'], 'chain-answer': ['repositoryRoot', 'chainId', 'answer'],
|
|
56
|
-
'branch-create-virtual': ['repositoryRoot', 'name', 'fromRef'], 'chain-ledger-init': ['repositoryRoot', 'plan'], 'chain-ledger-status': ['repositoryRoot', 'chainId'], 'chain-ledger-query': ['repositoryRoot'],
|
|
57
|
-
};
|
|
58
|
-
export const LOCAL_LEDGER_SCHEMAS = Object.freeze(Object.fromEntries(Object.keys(LOCAL_LEDGER_OPERATIONS).map(operation => [operation, { type: 'object', additionalProperties: false, required: LEDGER_REQUIRED[operation] ?? [] }])));
|
|
64
|
+
export const LOCAL_LEDGER_SCHEMAS = operationSchemasFor(LOCAL_LEDGER_OPERATIONS);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { KeyObject } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export interface MergeInspectionOptions {
|
|
4
|
+
publicKey?: string;
|
|
5
|
+
now?: number;
|
|
6
|
+
consumedNonces?: ReadonlySet<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface MergePreflightInspectionOptions extends MergeInspectionOptions {
|
|
9
|
+
preparation?: unknown;
|
|
10
|
+
mergeLease?: unknown;
|
|
11
|
+
preparationPublicKey?: string;
|
|
12
|
+
authorityPublicKey?: string;
|
|
13
|
+
consumedPreparationNonces?: ReadonlySet<string>;
|
|
14
|
+
consumedLeaseNonces?: ReadonlySet<string>;
|
|
15
|
+
}
|
|
16
|
+
export interface MergeReceiptDetails {
|
|
17
|
+
authentic?: boolean;
|
|
18
|
+
status?: 'passed' | 'blocked';
|
|
19
|
+
reason?: string;
|
|
20
|
+
preparation?: Record<string, unknown>;
|
|
21
|
+
preflight?: Record<string, unknown>;
|
|
22
|
+
attestation?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export type MergeReceiptInspection = MergeReceiptDetails & (
|
|
25
|
+
{ verified: true; keyId: string; nonce: string; receiptDigest: string }
|
|
26
|
+
| { verified: false; keyId?: string; nonce?: string; receiptDigest?: string }
|
|
27
|
+
);
|
|
28
|
+
export const MERGE_ARCHGUARD_SCHEMA: 'entrorove.archguard-merge-preflight/1.0';
|
|
29
|
+
export const MERGE_PREPARATION_SCHEMA: 'entrorove.merge-preparation-receipt/1.0';
|
|
30
|
+
export const MERGE_PIN_ENV: 'ENTROROVE_ARCHGUARD_RUNNER_PUBLIC_KEY';
|
|
31
|
+
export const MERGE_ARCHGUARD_RUNTIME_SHA256: string;
|
|
32
|
+
export const SHA256: RegExp;
|
|
33
|
+
export const ID: RegExp;
|
|
34
|
+
export function mergeSha256(bytes: string | Uint8Array): string;
|
|
35
|
+
export function mergeCanonical(value: unknown): string;
|
|
36
|
+
export function configuredMergePin(publicKey: string | undefined): { key: KeyObject; der: Buffer; keyId: string; publicKey: string };
|
|
37
|
+
export function verifyMergeSignature(value: unknown, signature: unknown, keyId: unknown, publicKey: string | undefined): string;
|
|
38
|
+
export function verifyMergeLifetime(value: Record<string, unknown>, now?: number, consumedNonces?: ReadonlySet<string>): void;
|
|
39
|
+
export function inspectMergeSignedEnvelope(value: unknown, options: MergeInspectionOptions & { schemaVersion: string }): MergeReceiptInspection;
|
|
40
|
+
export function inspectLegacyArchguardPreflight(value: unknown, binding?: Record<string, unknown>, options?: MergeInspectionOptions): MergeReceiptInspection;
|
|
41
|
+
export function mergeArtifactSha256(files: ReadonlyArray<{ path: string; sha256: string }>): string;
|
|
42
|
+
export function mergePreflightBinding(preparation: Record<string, unknown>): Record<string, unknown>;
|
|
43
|
+
export function inspectMergePreparation(preparation: unknown, binding?: Record<string, unknown>, options?: MergeInspectionOptions): MergeReceiptInspection;
|
|
44
|
+
export function inspectMergeArchguardPreflight(preflight: unknown, binding?: Record<string, unknown>, options?: MergePreflightInspectionOptions): MergeReceiptInspection;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const MERGE_ARCHGUARD_SCHEMA = 'entrorove.archguard-merge-preflight/1.0';
|
|
4
|
+
export const MERGE_PREPARATION_SCHEMA = 'entrorove.merge-preparation-receipt/1.0';
|
|
5
|
+
export const MERGE_PIN_ENV = 'ENTROROVE_ARCHGUARD_RUNNER_PUBLIC_KEY';
|
|
6
|
+
export const SHA256 = /^[0-9a-f]{64}$/;
|
|
7
|
+
export const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
|
+
const idValid = value => typeof value === 'string' && ID.test(value);
|
|
9
|
+
export const mergeSha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export const MERGE_ARCHGUARD_RUNTIME_SHA256 = '836f20b270139371bef43e3934fa7232371e42d46371c513db2a5e3c5622b215';
|
|
11
|
+
const MISSING_SHA256 = mergeSha256('entrorove.missing-file/1.0');
|
|
12
|
+
const CONTEXT_FIELDS = ['repositoryDigest', 'flowId', 'stepId', 'baselineRef', 'sourceRef', 'targetRef', 'paths',
|
|
13
|
+
'sourceFiles', 'targetFiles', 'afterManifest', 'beforeFullManifest', 'mergedArtifactSha256', 'snapshotDigest',
|
|
14
|
+
'snapshotReceiptDigest', 'architectureContractDigest', 'mappings', 'mappingProof', 'mappingDigest', 'mergeLeaseDigest', 'architectureLedgerDigest'];
|
|
15
|
+
const LEASE_CONTEXT = ['mergeId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'scopePaths'];
|
|
16
|
+
const OPTIONAL_CONTEXT = ['handoffEpoch'];
|
|
17
|
+
|
|
18
|
+
export function mergeCanonical(value, ancestors = new Set()) {
|
|
19
|
+
if (value === null || ['string', 'boolean'].includes(typeof value)) return JSON.stringify(value);
|
|
20
|
+
if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);
|
|
21
|
+
if (!value || typeof value !== 'object' || ancestors.has(value)
|
|
22
|
+
|| (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) {
|
|
23
|
+
throw new Error('Merge evidence must be finite plain JSON');
|
|
24
|
+
}
|
|
25
|
+
const next = new Set(ancestors).add(value);
|
|
26
|
+
return Array.isArray(value) ? `[${value.map(item => mergeCanonical(item, next)).join(',')}]`
|
|
27
|
+
: `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${mergeCanonical(value[key], next)}`).join(',')}}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function configuredMergePin(publicKey) {
|
|
31
|
+
if (typeof publicKey !== 'string' || !publicKey) throw new Error('An independently configured Archguard runner pin is required');
|
|
32
|
+
const der = Buffer.from(publicKey, 'base64');
|
|
33
|
+
if (der.toString('base64') !== publicKey) throw new Error('Archguard runner pin must be canonical base64');
|
|
34
|
+
const key = createPublicKey({ key: der, type: 'spki', format: 'der' });
|
|
35
|
+
if (key.asymmetricKeyType !== 'ed25519' || !key.export({ type: 'spki', format: 'der' }).equals(der)) {
|
|
36
|
+
throw new Error('Archguard runner pin must be canonical Ed25519 SPKI');
|
|
37
|
+
}
|
|
38
|
+
return { key, der, keyId: mergeSha256(der), publicKey };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function verifyMergeSignature(value, signature, keyId, publicKey) {
|
|
42
|
+
const pin = configuredMergePin(publicKey);
|
|
43
|
+
if (typeof signature !== 'string' || !/^[A-Za-z0-9_-]{86}$/.test(signature)) throw new Error('Archguard signature must be canonical base64url');
|
|
44
|
+
const bytes = Buffer.from(signature, 'base64url');
|
|
45
|
+
if (bytes.length !== 64 || bytes.toString('base64url') !== signature || keyId !== pin.keyId) throw new Error('Archguard signature or key identity is invalid');
|
|
46
|
+
const source = mergeCanonical(value);
|
|
47
|
+
if (Buffer.byteLength(source) > 4 * 1024 * 1024 || !verify(null, Buffer.from(source), pin.key, bytes)) throw new Error('Archguard signature is invalid');
|
|
48
|
+
return pin.keyId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function verifyMergeLifetime(value, now = Date.now(), consumedNonces) {
|
|
52
|
+
const issued = Date.parse(value.issuedAt), expires = Date.parse(value.expiresAt);
|
|
53
|
+
if (!Number.isFinite(now) || typeof value.issuedAt !== 'string' || typeof value.expiresAt !== 'string'
|
|
54
|
+
|| !Number.isFinite(issued) || !Number.isFinite(expires) || issued > now || expires <= now || expires <= issued
|
|
55
|
+
|| expires - issued > 300_000 || !idValid(value.nonce) || consumedNonces?.has(value.nonce)) {
|
|
56
|
+
throw new Error('Archguard receipt lifetime, nonce, or replay state is invalid');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function inspectMergeSignedEnvelope(value, options = {}) {
|
|
61
|
+
try {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.schemaVersion !== options.schemaVersion) throw new Error('Merge receipt schema is invalid');
|
|
63
|
+
const { signature, ...unsigned } = value;
|
|
64
|
+
verifyMergeLifetime(value, options.now, options.consumedNonces);
|
|
65
|
+
const keyId = verifyMergeSignature(unsigned, signature, value.keyId,
|
|
66
|
+
Object.hasOwn(options, 'publicKey') ? options.publicKey : process.env[MERGE_PIN_ENV]?.trim());
|
|
67
|
+
return { verified: true, keyId, nonce: value.nonce, receiptDigest: mergeSha256(mergeCanonical(value)) };
|
|
68
|
+
} catch (error) { return { verified: false, reason: error instanceof Error ? error.message : String(error) }; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function inspectLegacyArchguardPreflight(attestation, binding = {}, options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
if (!attestation || attestation.schemaVersion !== 'entrorove.archguard-preflight/1.0' || attestation.status !== 'passed'
|
|
74
|
+
|| !idValid(attestation.flowId) || !idValid(attestation.stepId)
|
|
75
|
+
|| !SHA256.test(String(attestation.snapshotDigest)) || !SHA256.test(String(attestation.contractDigest))) {
|
|
76
|
+
throw new Error('Archguard preflight is missing or not passed');
|
|
77
|
+
}
|
|
78
|
+
const keys = ['flowId', 'stepId', 'snapshotDigest', 'contractDigest'];
|
|
79
|
+
for (const key of ['mergeDigest', 'preparationDigest', 'sourceRef', 'targetRef']) if (Object.hasOwn(binding, key)) keys.push(key);
|
|
80
|
+
if (keys.some(key => !Object.hasOwn(binding, key) || mergeCanonical(attestation[key]) !== mergeCanonical(binding[key]))) throw new Error('Archguard preflight frozen binding does not match');
|
|
81
|
+
const { receipt, ...unsigned } = attestation;
|
|
82
|
+
if (!receipt || receipt.schemaVersion !== 'entrorove.runner-receipt/1.0') throw new Error('Archguard signed receipt is missing');
|
|
83
|
+
verifyMergeLifetime(attestation, options.now, options.consumedNonces);
|
|
84
|
+
const keyId = verifyMergeSignature(unsigned, receipt.signature, receipt.keyId,
|
|
85
|
+
Object.hasOwn(options, 'publicKey') ? options.publicKey : process.env[MERGE_PIN_ENV]?.trim());
|
|
86
|
+
return { verified: true, attestation, keyId, nonce: attestation.nonce, receiptDigest: mergeSha256(mergeCanonical(attestation)) };
|
|
87
|
+
} catch (error) { return { verified: false, reason: error instanceof Error ? error.message : String(error) }; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireValue(condition, message) { if (!condition) throw new Error(message); }
|
|
91
|
+
function canonicalEqual(left, right) { return mergeCanonical(left) === mergeCanonical(right); }
|
|
92
|
+
function pathValid(value) {
|
|
93
|
+
return typeof value === 'string' && value.length <= 500 && value === value.trim() && value === value.normalize('NFC')
|
|
94
|
+
&& !/^[~\/]|[\\:\u0000-\u001f\u007f]/.test(value) && value.split('/').every(part => part && part !== '.' && part !== '..');
|
|
95
|
+
}
|
|
96
|
+
function validateManifest(files, source = false) {
|
|
97
|
+
requireValue(Array.isArray(files) && files.length > 0 && files.length <= 512, 'Merge file manifest is missing or unbounded');
|
|
98
|
+
const names = files.map(file => file?.path);
|
|
99
|
+
requireValue(names.every(pathValid) && canonicalEqual(names, [...new Set(names)].sort()), 'Merge manifest paths must be canonical, sorted and unique');
|
|
100
|
+
requireValue(!names.some((name, index) => names.slice(index + 1).some(other => other.startsWith(`${name}/`))), 'Merge manifest paths overlap');
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const fields = source ? 'exists|mode|path|sha256|sourcePath' : 'exists|mode|path|sha256';
|
|
103
|
+
requireValue(Object.keys(file).sort().join('|') === fields && (!source || pathValid(file.sourcePath)), 'Merge file origin or metadata shape is invalid');
|
|
104
|
+
requireValue(file && typeof file.exists === 'boolean' && SHA256.test(String(file.sha256))
|
|
105
|
+
&& (file.exists ? Number.isSafeInteger(file.mode) && file.mode >= 0 && file.mode <= 0o777 : file.mode === null && file.sha256 === MISSING_SHA256), 'Merge manifest file metadata is invalid');
|
|
106
|
+
requireValue(!source || file.exists, 'Merge source deletion is explicitly unsupported');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateMapping(value, source) {
|
|
110
|
+
const proof = value.mappingProof;
|
|
111
|
+
requireValue(proof?.schemaVersion === 'entrorove.merge-artifact-mapping/1.0' && SHA256.test(String(value.mappingDigest))
|
|
112
|
+
&& mergeSha256(mergeCanonical(proof)) === value.mappingDigest, 'Merge mapping must retain its complete original frozen proof');
|
|
113
|
+
requireBinding(proof, value, ['flowId', 'ownerId', 'taskDigest', 'chainDigest']);
|
|
114
|
+
requireValue(idValid(proof.planId) && SHA256.test(String(proof.planDigest)) && SHA256.test(String(proof.scopeArtifactDigest))
|
|
115
|
+
&& proof.blueprintBinding && typeof proof.blueprintBinding === 'object' && !Array.isArray(proof.blueprintBinding)
|
|
116
|
+
&& Object.keys(proof.blueprintBinding).length > 0, 'Merge mapping original plan or Blueprint ancestry is missing');
|
|
117
|
+
requireValue(Array.isArray(proof.entries) && proof.entries.length > 0 && proof.entries.length <= 512
|
|
118
|
+
&& proof.entries.every(entry => entry && pathValid(entry.sourcePath) && pathValid(entry.targetPath)
|
|
119
|
+
&& SHA256.test(String(entry.sha256)) && idValid(entry.producerNodeRef) && idValid(entry.producerStepId) && idValid(entry.stepId)), 'Merge mapping producer entries are invalid');
|
|
120
|
+
const targets = proof.entries.map(entry => entry.targetPath), origins = proof.entries.map(entry => entry.sourcePath);
|
|
121
|
+
requireValue(canonicalEqual(targets, [...new Set(targets)].sort()) && new Set(origins).size === origins.length, 'Merge mapping origins or targets are duplicated');
|
|
122
|
+
const expected = source.map(file => ({ sourcePath: file.sourcePath, targetPath: file.path }));
|
|
123
|
+
requireValue(canonicalEqual(value.mappings, expected), 'Merge source origins do not match their exact immutable mapping');
|
|
124
|
+
for (const file of source) requireValue(proof.entries.some(entry => entry.sourcePath === file.sourcePath
|
|
125
|
+
&& entry.targetPath === file.path && entry.sha256 === file.sha256), 'Merge mapping source is not the original declared producer artifact');
|
|
126
|
+
}
|
|
127
|
+
export function mergeArtifactSha256(files) {
|
|
128
|
+
return mergeSha256(`validator.file-manifest/1.0\n${files.map(file => `${file.path}\0${file.sha256}`).join('\n')}`);
|
|
129
|
+
}
|
|
130
|
+
function validateRef(ref, role, preparation, files) {
|
|
131
|
+
requireValue(ref?.kind === 'snapshot' && idValid(ref.artifactId) && SHA256.test(String(ref.manifestDigest)), 'Merge ref must identify a frozen snapshot');
|
|
132
|
+
const manifest = { schemaVersion: 'entrorove.merge-artifact/1.0', artifactId: ref.artifactId, role,
|
|
133
|
+
repositoryDigest: preparation.repositoryDigest, flowId: preparation.flowId, stepId: preparation.stepId, files };
|
|
134
|
+
requireValue(ref.manifestDigest === mergeSha256(mergeCanonical(manifest)), 'Merge reference does not bind the exact frozen manifest');
|
|
135
|
+
}
|
|
136
|
+
export function mergePreflightBinding(preparation) {
|
|
137
|
+
const fields = [...CONTEXT_FIELDS, ...LEASE_CONTEXT, ...OPTIONAL_CONTEXT.filter(key => Object.hasOwn(preparation, key))];
|
|
138
|
+
return { ...Object.fromEntries(fields.map(key => [key, preparation[key]])),
|
|
139
|
+
preparationDigest: mergeSha256(mergeCanonical(preparation)), mergeDigest: preparation.mergedArtifactSha256 };
|
|
140
|
+
}
|
|
141
|
+
function validatePreparation(value) {
|
|
142
|
+
requireValue(/^[0-9a-f]{32}$/.test(String(value.repositoryDigest)) && idValid(value.flowId) && idValid(value.stepId), 'Merge preparation repository, flow or step is invalid');
|
|
143
|
+
requireValue(typeof value.baselineRef === 'string' && value.baselineRef.trim() === value.baselineRef && value.baselineRef.length > 0 && value.baselineRef.length <= 2_000, 'Merge baseline reference is required');
|
|
144
|
+
for (const key of ['mergedArtifactSha256', 'snapshotDigest', 'snapshotReceiptDigest', 'architectureContractDigest', 'mergeLeaseDigest', 'architectureLedgerDigest']) requireValue(SHA256.test(String(value[key])), `Merge preparation ${key} is invalid`);
|
|
145
|
+
validateManifest(value.beforeFullManifest);
|
|
146
|
+
const source = value.sourceFiles?.map(file => ({ ...file, exists: true })); validateManifest(source, true);
|
|
147
|
+
validateManifest(value.targetFiles); validateMapping(value, source);
|
|
148
|
+
const mappedPaths = source.map(file => file.path);
|
|
149
|
+
const mappedBefore = value.beforeFullManifest.filter(file => mappedPaths.includes(file.path));
|
|
150
|
+
requireValue(canonicalEqual(value.targetFiles, mappedBefore), 'Merge target batch differs from its wide frozen target');
|
|
151
|
+
const nativeFiles = source.map(({ path, sha256 }) => ({ path, sha256 }));
|
|
152
|
+
requireValue(canonicalEqual(value.afterManifest, nativeFiles) && value.mergedArtifactSha256 === mergeArtifactSha256(nativeFiles), 'Merge actual artifact hash or manifest is inconsistent');
|
|
153
|
+
requireValue(canonicalEqual(mappedPaths, value.targetFiles.map(file => file.path)), 'Merge source and target batch path sets differ');
|
|
154
|
+
const changed = source.filter(({ sourcePath, ...file }, index) => !canonicalEqual(file, value.targetFiles[index])).map(file => file.path);
|
|
155
|
+
requireValue(changed.length > 0 && canonicalEqual(value.paths, changed), 'Merge requires exact nonempty actual changes');
|
|
156
|
+
requireValue(source.some((file, index) => !value.targetFiles[index].exists || file.sha256 !== value.targetFiles[index].sha256), 'Merge requires actual nonempty byte changes; modes alone cannot authorize it');
|
|
157
|
+
validateRef(value.sourceRef, 'source', value, source); validateRef(value.targetRef, 'target', value, value.beforeFullManifest);
|
|
158
|
+
requireValue(value.sourceRef.artifactId !== value.targetRef.artifactId, 'Merge source and target refs must be distinct');
|
|
159
|
+
for (const key of ['mergeId', 'taskId', 'ownerId', 'leaseId']) requireValue(idValid(value[key]), `Merge ${key} is invalid`);
|
|
160
|
+
for (const key of ['taskDigest', 'chainDigest']) requireValue(SHA256.test(String(value[key])), `Merge ${key} is invalid`);
|
|
161
|
+
requireValue(Number.isSafeInteger(value.generation) && value.generation > 0 && Number.isSafeInteger(value.epoch) && value.epoch >= 0, 'Merge lease generation or epoch is invalid');
|
|
162
|
+
requireValue(canonicalEqual(value.scopePaths, value.beforeFullManifest.map(file => file.path)), 'Merge preparation must cover its entire wide original snapshot');
|
|
163
|
+
if (Object.hasOwn(value, 'handoffEpoch')) requireValue(Number.isSafeInteger(value.handoffEpoch) && value.handoffEpoch >= 0, 'Merge handoff epoch is invalid');
|
|
164
|
+
}
|
|
165
|
+
function requireBinding(value, binding, fields) {
|
|
166
|
+
for (const key of fields) if (Object.hasOwn(binding, key)) requireValue(canonicalEqual(value[key], binding[key]), `Merge frozen ${key} binding does not match`);
|
|
167
|
+
}
|
|
168
|
+
export function inspectMergePreparation(preparation, binding = {}, options = {}) {
|
|
169
|
+
const signed = inspectMergeSignedEnvelope(preparation, { ...options, schemaVersion: MERGE_PREPARATION_SCHEMA,
|
|
170
|
+
publicKey: options.publicKey ?? process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY?.trim() });
|
|
171
|
+
if (!signed.verified) return signed;
|
|
172
|
+
try {
|
|
173
|
+
validatePreparation(preparation); requireBinding(preparation, binding, [...CONTEXT_FIELDS, ...LEASE_CONTEXT, ...OPTIONAL_CONTEXT]);
|
|
174
|
+
return { ...signed, preparation };
|
|
175
|
+
} catch (error) { return { verified: false, reason: error instanceof Error ? error.message : String(error) }; }
|
|
176
|
+
}
|
|
177
|
+
function validateNativeCheckpoint(result, source, target, preflight) {
|
|
178
|
+
const { checkpoint, ledgerEntry } = result;
|
|
179
|
+
requireValue(result.path === source.path && result.sourcePath === source.sourcePath && result.sha256 === source.sha256 && result.beforeSha256 === target.sha256
|
|
180
|
+
&& result.contentSha256 === source.sha256 && result.exists === true, 'Archguard checkpoint input is not the actual frozen artifact');
|
|
181
|
+
requireValue(checkpoint && ['passed', 'blocked'].includes(checkpoint.status) && typeof checkpoint.writeAllowed === 'boolean'
|
|
182
|
+
&& typeof checkpoint.rollbackRequired === 'boolean' && checkpoint.writeAllowed === !checkpoint.rollbackRequired
|
|
183
|
+
&& (checkpoint.status === 'passed') === checkpoint.writeAllowed, 'Archguard checkpoint verdict is inconsistent');
|
|
184
|
+
requireValue(ledgerEntry?.schemaVersion === 'archguard.checkpoint-ledger/1.0' && SHA256.test(String(ledgerEntry.checkpointId))
|
|
185
|
+
&& idValid(ledgerEntry.blockId) && ledgerEntry.path === source.path && ledgerEntry.beforeSha256 === target.sha256
|
|
186
|
+
&& ledgerEntry.afterSha256 === source.sha256 && ledgerEntry.contractRevision === preflight.contractRevision
|
|
187
|
+
&& ledgerEntry.status === checkpoint.status && ledgerEntry.rollbackRequired === checkpoint.rollbackRequired, 'Archguard native ledger does not bind the exact checkpoint');
|
|
188
|
+
const metrics = checkpoint.metrics;
|
|
189
|
+
requireValue(metrics?.path === source.path && typeof metrics.complexityChecked === 'boolean'
|
|
190
|
+
&& ['first-party', 'third-party', 'generated', 'dependency-lockfile'].includes(metrics.inspectionScope)
|
|
191
|
+
&& ['fileLines', 'maxNestingDepth', 'maxFunctionLines', 'animations', 'transitions'].every(key => Number.isSafeInteger(metrics[key]) && metrics[key] >= 0), 'Archguard native metrics are missing or invalid');
|
|
192
|
+
requireValue(Array.isArray(checkpoint.findings) && checkpoint.findings.every(finding => finding && typeof finding.blocking === 'boolean'
|
|
193
|
+
&& typeof finding.ruleId === 'string' && finding.ruleId && finding.entityRef === source.path && ['P0', 'P1', 'P2'].includes(finding.severity)), 'Archguard native findings are malformed');
|
|
194
|
+
const rules = [...new Set(checkpoint.findings.map(finding => finding.ruleId))].sort();
|
|
195
|
+
const blocking = [...new Set(checkpoint.findings.filter(finding => finding.blocking).map(finding => finding.ruleId))].sort();
|
|
196
|
+
requireValue(canonicalEqual(ledgerEntry.ruleIds, rules) && canonicalEqual(ledgerEntry.blockingRuleIds, blocking)
|
|
197
|
+
&& (!blocking.length || checkpoint.status === 'blocked'), 'Archguard native rule verdicts disagree');
|
|
198
|
+
requireValue(checkpoint.drift && ['green', 'yellow', 'red'].includes(checkpoint.drift.trend)
|
|
199
|
+
&& (checkpoint.drift.trend !== 'red' || checkpoint.status === 'blocked'), 'Archguard native drift verdict is invalid');
|
|
200
|
+
}
|
|
201
|
+
export function inspectMergeArchguardPreflight(preflight, binding = {}, options = {}) {
|
|
202
|
+
const signed = inspectMergeSignedEnvelope(preflight, { ...options, schemaVersion: MERGE_ARCHGUARD_SCHEMA });
|
|
203
|
+
if (!signed.verified) return signed;
|
|
204
|
+
try {
|
|
205
|
+
const preparation = options.preparation;
|
|
206
|
+
const prepared = inspectMergePreparation(preparation, binding, { publicKey: options.preparationPublicKey, now: options.now,
|
|
207
|
+
consumedNonces: options.consumedPreparationNonces ?? options.consumedNonces });
|
|
208
|
+
requireValue(prepared.verified, prepared.reason ?? 'Original merge preparation is required');
|
|
209
|
+
const lease = options.mergeLease;
|
|
210
|
+
const leased = inspectMergeSignedEnvelope(lease, { schemaVersion: 'entrorove.merge-lease/1.0', now: options.now,
|
|
211
|
+
publicKey: options.authorityPublicKey ?? process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(),
|
|
212
|
+
consumedNonces: options.consumedLeaseNonces ?? options.consumedNonces });
|
|
213
|
+
requireValue(leased.verified, leased.reason ?? 'Original signed merge lease is required');
|
|
214
|
+
const leaseFields = [...LEASE_CONTEXT, 'repositoryDigest', 'flowId', 'stepId', 'baselineRef', 'architectureContractDigest', 'mappings', 'mappingProof', 'mappingDigest'];
|
|
215
|
+
requireValue(leaseFields.every(key => Object.hasOwn(lease, key)), 'Original merge lease fields are missing');
|
|
216
|
+
requireBinding(preparation, lease, leaseFields);
|
|
217
|
+
requireValue(canonicalEqual(preparation.sourceFiles.map(file => file.path), lease.paths) && SHA256.test(String(lease.architectureLedgerDigest))
|
|
218
|
+
&& preparation.mergeLeaseDigest === leased.receiptDigest && preparation.architectureLedgerDigest === lease.architectureLedgerDigest
|
|
219
|
+
&& preflight.mergeLeaseDigest === leased.receiptDigest && preflight.architectureLedgerDigest === lease.architectureLedgerDigest
|
|
220
|
+
&& lease.mappingProof.planId === lease.planId && lease.mappingProof.planDigest === lease.planDigest
|
|
221
|
+
&& SHA256.test(String(lease.scopeContractArtifactDigest)) && lease.mappingProof.scopeArtifactDigest === lease.scopeContractArtifactDigest
|
|
222
|
+
&& preflight.scopeContractArtifactDigest === lease.scopeContractArtifactDigest,
|
|
223
|
+
'Archguard original authority lease or frozen architecture ledger binding is invalid');
|
|
224
|
+
const expected = mergePreflightBinding(preparation);
|
|
225
|
+
requireBinding(preflight, expected, Object.keys(expected)); requireBinding(preflight, binding, [...Object.keys(expected), 'contractRawDigest', 'contractRevision', 'contractId', 'nativeLedgerHeadDigest']);
|
|
226
|
+
requireValue(preflight.nativeRuntimeSha256 === MERGE_ARCHGUARD_RUNTIME_SHA256 && preflight.architectureContract?.schemaVersion === 'archguard.contract/1.0'
|
|
227
|
+
&& mergeSha256(mergeCanonical(preflight.architectureContract)) === preflight.architectureContractDigest
|
|
228
|
+
&& preflight.contractId === preflight.architectureContract.contractId && preflight.contractRevision === preflight.architectureContract.revision
|
|
229
|
+
&& Number.isSafeInteger(preflight.contractRevision) && preflight.contractRevision > 0 && SHA256.test(String(preflight.contractRawDigest))
|
|
230
|
+
&& SHA256.test(String(preflight.nativeLedgerHeadDigest)), 'Archguard locked native contract or actual native ledger head is missing or inconsistent');
|
|
231
|
+
requireValue(Date.parse(preflight.expiresAt) <= Math.min(Date.parse(preparation.expiresAt), Date.parse(lease.expiresAt))
|
|
232
|
+
&& Date.parse(preflight.issuedAt) >= Math.max(Date.parse(preparation.issuedAt), Date.parse(lease.issuedAt)), 'Archguard preflight outlives its original frozen preparation or lease');
|
|
233
|
+
requireValue(Array.isArray(preflight.results) && preflight.results.length === preparation.sourceFiles.length, 'Every actual output file requires its own native checkpoint');
|
|
234
|
+
preflight.results.forEach((result, index) => validateNativeCheckpoint(result, preparation.sourceFiles[index], preparation.targetFiles[index], preflight));
|
|
235
|
+
const passed = preflight.results.every(result => result.checkpoint.status === 'passed');
|
|
236
|
+
requireValue(preflight.status === (passed ? 'passed' : 'blocked'), 'Archguard preflight summary disagrees with native checkpoints');
|
|
237
|
+
return { ...signed, verified: passed, authentic: true, status: preflight.status, preflight,
|
|
238
|
+
...(passed ? {} : { reason: 'Actual native Archguard checkpoint blocked the merge' }) };
|
|
239
|
+
} catch (error) { return { verified: false, reason: error instanceof Error ? error.message : String(error) }; }
|
|
240
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { context, authority, readBytes, readLedger, withExclusiveLock, fail } from './local-archguard-state.mjs';
|
|
4
|
+
import { contractFile, loadContractLock } from './local-archguard-contract.mjs';
|
|
5
|
+
import { history, drift } from './local-archguard-snapshot.mjs';
|
|
6
|
+
import { evaluateArchguardCheckpoint } from './local-archguard-evaluate.mjs';
|
|
7
|
+
import { signRunnerReceipt } from './local-archguard-signing.mjs';
|
|
8
|
+
import { readFrozenMergeArtifact } from './local-merge-artifacts.mjs';
|
|
9
|
+
import { context as mergeContext, withMergeFence } from './local-merge-storage.mjs';
|
|
10
|
+
import { inspectMergeLease } from './local-merge-authority.mjs';
|
|
11
|
+
import { configuredMergePin, inspectMergePreparation, inspectMergeArchguardPreflight, mergePreflightBinding,
|
|
12
|
+
mergeCanonical, mergeSha256, SHA256, MERGE_ARCHGUARD_SCHEMA, MERGE_ARCHGUARD_RUNTIME_SHA256 } from './local-merge-archguard-verification.mjs';
|
|
13
|
+
|
|
14
|
+
const equal = (left, right) => mergeCanonical(left) === mergeCanonical(right);
|
|
15
|
+
function requireValue(condition, code, message) { if (!condition) fail(code, message); }
|
|
16
|
+
|
|
17
|
+
async function lockedContract(ctx, expected) {
|
|
18
|
+
const current = await contractFile(ctx), locked = await loadContractLock(ctx);
|
|
19
|
+
requireValue(locked.usedConfirmations?.length > 0 && locked.repositoryDigest === ctx.repositoryDigest
|
|
20
|
+
&& current.contractDigest === locked.contractDigest && current.rawDigest === locked.rawDigest
|
|
21
|
+
&& current.contract.revision === locked.revision && current.contractDigest === expected,
|
|
22
|
+
'MERGE_ARCH_CONTRACT', 'Merge preflight requires the unchanged explicitly confirmed original architecture contract');
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
async function artifacts(input, preparation) {
|
|
26
|
+
const source = await readFrozenMergeArtifact(input, preparation.sourceRef);
|
|
27
|
+
const target = await readFrozenMergeArtifact(input, preparation.targetRef);
|
|
28
|
+
const after = source.manifest.files.map(({ exists, ...file }) => file);
|
|
29
|
+
requireValue(source.manifest.role === 'source' && target.manifest.role === 'target'
|
|
30
|
+
&& source.manifest.files.every(file => file.exists) && equal(after, preparation.sourceFiles)
|
|
31
|
+
&& equal(target.manifest.files, preparation.beforeFullManifest),
|
|
32
|
+
'MERGE_ARCH_ARTIFACT', 'Archguard reads only the exact frozen source and target manifests');
|
|
33
|
+
return { source, target };
|
|
34
|
+
}
|
|
35
|
+
async function assertTarget(ctx, expected) {
|
|
36
|
+
for (const file of expected) {
|
|
37
|
+
const actual = await readBytes(path.join(ctx.root, file.path), !file.exists, ctx);
|
|
38
|
+
requireValue(actual.exists === file.exists && (!file.exists || mergeSha256(actual.content) === file.sha256 && actual.mode === file.mode),
|
|
39
|
+
'MERGE_ARCH_TARGET_CHANGED', 'Actual target differs from its frozen original merge snapshot');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function scanCandidate(current, source, target, previous) {
|
|
43
|
+
const results = [], entries = [...previous], decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
44
|
+
const before = new Map(target.files.map(file => [file.path, file]));
|
|
45
|
+
for (const [index, file] of source.files.entries()) {
|
|
46
|
+
const content = decoder.decode(file.bytes);
|
|
47
|
+
requireValue(mergeSha256(Buffer.from(content)) === file.sha256, 'MERGE_ARCH_ENCODING', 'Archguard requires unchanged valid UTF-8 source bytes');
|
|
48
|
+
const result = await evaluateArchguardCheckpoint(current.contract, { blockId: `merge-${index + 1}`, path: file.path,
|
|
49
|
+
content, beforeSha256: before.get(file.path).sha256 }, entries);
|
|
50
|
+
entries.push(result.ledgerEntry);
|
|
51
|
+
results.push({ path: file.path, exists: true, sha256: file.sha256, contentSha256: file.sha256,
|
|
52
|
+
sourcePath: file.sourcePath, beforeSha256: before.get(file.path).sha256, ...result });
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
async function produceLocked(ctx, input, lease) {
|
|
57
|
+
const preparation = input.preparation, pin = configuredMergePin(process.env.ENTROROVE_ARCHGUARD_RUNNER_PUBLIC_KEY?.trim()), config = await authority(ctx);
|
|
58
|
+
requireValue(config.runnerPublicKey === pin.publicKey, 'MERGE_ARCH_RUNNER_PIN', 'Original Archguard runner signer must match the independently configured preflight pin');
|
|
59
|
+
const binding = { repositoryDigest: ctx.repositoryDigest, flowId: ctx.flowId, stepId: input.stepId,
|
|
60
|
+
...Object.fromEntries(['mergeId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'architectureContractDigest', 'baselineRef'].map(key => [key, lease[key]])),
|
|
61
|
+
mappings: lease.mappings, mappingProof: lease.mappingProof, mappingDigest: lease.mappingDigest, scopePaths: lease.scopePaths };
|
|
62
|
+
const prepared = inspectMergePreparation(preparation, binding);
|
|
63
|
+
requireValue(prepared.verified, 'MERGE_ARCH_PREPARATION', prepared.reason ?? 'Original signed merge preparation is required');
|
|
64
|
+
const current = await lockedContract(ctx, preparation.architectureContractDigest), ledger = await readLedger(ctx);
|
|
65
|
+
requireValue(SHA256.test(String(ledger.headDigest)), 'MERGE_ARCH_LEDGER_UNMANAGED', 'Merge preflight requires an actual managed native ledger head');
|
|
66
|
+
requireValue(!drift(ledger).interrupted, 'MERGE_ARCH_RED_INTERRUPT', 'Archguard red interrupt requires trusted human direction before preflight');
|
|
67
|
+
const frozen = await artifacts(input, preparation); await assertTarget(ctx, frozen.target.files);
|
|
68
|
+
const results = await scanCandidate(current, frozen.source, frozen.target, history(ledger));
|
|
69
|
+
const fresh = await artifacts(input, preparation); await assertTarget(ctx, fresh.target.files);
|
|
70
|
+
const unchanged = await lockedContract(ctx, preparation.architectureContractDigest), latest = await readLedger(ctx);
|
|
71
|
+
requireValue(fresh.source.files.every((file, index) => file.bytes.equals(frozen.source.files[index].bytes))
|
|
72
|
+
&& unchanged.rawDigest === current.rawDigest && latest.headDigest === ledger.headDigest,
|
|
73
|
+
'MERGE_ARCH_SCAN_RACE', 'Frozen artifacts, architecture contract or checkpoint history changed during actual scan');
|
|
74
|
+
requireValue(inspectMergePreparation(preparation, binding).verified, 'MERGE_ARCH_PREPARATION_EXPIRED', 'Original preparation expired during actual Archguard scan');
|
|
75
|
+
const now = Date.now(), expires = Math.min(Date.parse(preparation.expiresAt), Date.parse(lease.expiresAt), now + 60_000);
|
|
76
|
+
const payload = { schemaVersion: MERGE_ARCHGUARD_SCHEMA, ...mergePreflightBinding(preparation),
|
|
77
|
+
status: results.every(result => result.checkpoint.status === 'passed') ? 'passed' : 'blocked',
|
|
78
|
+
architectureContract: current.contract, contractId: current.contract.contractId, contractRevision: current.contract.revision,
|
|
79
|
+
contractRawDigest: current.rawDigest, nativeLedgerHeadDigest: ledger.headDigest, nativeRuntimeSha256: MERGE_ARCHGUARD_RUNTIME_SHA256, results,
|
|
80
|
+
mergeLeaseDigest: mergeSha256(mergeCanonical(lease)), architectureLedgerDigest: lease.architectureLedgerDigest,
|
|
81
|
+
scopeContractArtifactDigest: lease.scopeContractArtifactDigest,
|
|
82
|
+
issuedAt: new Date(now).toISOString(), expiresAt: new Date(expires).toISOString(), nonce: randomUUID() };
|
|
83
|
+
const signed = await signRunnerReceipt(ctx, payload, true);
|
|
84
|
+
const checked = inspectMergeArchguardPreflight(signed, binding, { preparation, mergeLease: lease });
|
|
85
|
+
requireValue(checked.authentic, 'MERGE_ARCH_OUTPUT_INVALID', checked.reason ?? 'Produced native Archguard receipt failed its own independent inspection');
|
|
86
|
+
return signed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function produceMergeArchguardPreflight(input) {
|
|
90
|
+
const merge = await mergeContext(input);
|
|
91
|
+
return withMergeFence(merge, async (state, registration) => {
|
|
92
|
+
const lease = inspectMergeLease(input.mergeLease, merge, state, registration), ctx = await context(input);
|
|
93
|
+
return withExclusiveLock(path.join(ctx.repositoryState, 'mutation.lock'), () =>
|
|
94
|
+
withExclusiveLock(path.join(ctx.directory, 'mutation.lock'), () => produceLocked(ctx, input, lease)));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function nativeCurrent(ctx, binding) {
|
|
99
|
+
requireValue(binding && typeof binding === 'object' && !Array.isArray(binding), 'MERGE_ARCH_CONTRACT', 'Original architecture binding is required');
|
|
100
|
+
requireValue(SHA256.test(String(binding.architectureContractDigest)), 'MERGE_ARCH_CONTRACT', 'Original canonical architecture binding is required');
|
|
101
|
+
const current = await lockedContract(ctx, binding.architectureContractDigest), ledger = await readLedger(ctx);
|
|
102
|
+
requireValue(!drift(ledger).interrupted, 'MERGE_ARCH_RED_INTERRUPT', 'Native Archguard red interrupt prohibits a merge or rollback commit');
|
|
103
|
+
requireValue(SHA256.test(String(ledger.headDigest)), 'MERGE_ARCH_LEDGER_UNMANAGED', 'Actual managed native ledger head is required');
|
|
104
|
+
if (Object.hasOwn(binding, 'contractRawDigest')) requireValue(current.rawDigest === binding.contractRawDigest,
|
|
105
|
+
'MERGE_ARCH_CONTRACT', 'Actual architecture raw bytes changed after their original preflight');
|
|
106
|
+
if (Object.hasOwn(binding, 'nativeLedgerHeadDigest')) requireValue(ledger.headDigest === binding.nativeLedgerHeadDigest,
|
|
107
|
+
'MERGE_ARCH_LEDGER_CHANGED', 'Actual native checkpoint history changed after its original frozen head');
|
|
108
|
+
return { architectureContractDigest: current.contractDigest, contractRawDigest: current.rawDigest, nativeLedgerHeadDigest: ledger.headDigest };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Caller already holds the physical handoff and merge repository fences. Invoke
|
|
112
|
+
// assertCurrent inside the atomic finish/recovery boundary before publishing.
|
|
113
|
+
export async function withMergeArchguardStateFence(input, binding, action) {
|
|
114
|
+
requireValue(typeof action === 'function', 'MERGE_ARCH_ACTION', 'Native architecture fence requires its original action');
|
|
115
|
+
const ctx = await context(input);
|
|
116
|
+
return withExclusiveLock(path.join(ctx.repositoryState, 'mutation.lock'), () =>
|
|
117
|
+
withExclusiveLock(path.join(ctx.directory, 'mutation.lock'), async () => {
|
|
118
|
+
const facts = Object.freeze(await nativeCurrent(ctx, binding));
|
|
119
|
+
const assertCurrent = async () => { await nativeCurrent(ctx, facts); };
|
|
120
|
+
return action(assertCurrent, facts);
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function withMergeArchguardFence(input, preflight, action) {
|
|
125
|
+
requireValue(typeof action === 'function', 'MERGE_ARCH_ACTION', 'Native architecture fence requires its original action');
|
|
126
|
+
const ctx = await context(input), binding = { repositoryDigest: ctx.repositoryDigest, flowId: ctx.flowId, stepId: input.stepId };
|
|
127
|
+
const assertReceipt = () => {
|
|
128
|
+
const checked = inspectMergeArchguardPreflight(preflight, binding, { preparation: input.preparation, mergeLease: input.mergeLease });
|
|
129
|
+
requireValue(checked.verified && checked.authentic, 'MERGE_ARCH_PREFLIGHT', checked.reason ?? 'Original independent passing native preflight is required');
|
|
130
|
+
};
|
|
131
|
+
assertReceipt();
|
|
132
|
+
return withMergeArchguardStateFence(input, preflight, async assertNativeCurrent => {
|
|
133
|
+
const pin = configuredMergePin(process.env.ENTROROVE_ARCHGUARD_RUNNER_PUBLIC_KEY?.trim()), config = await authority(ctx);
|
|
134
|
+
requireValue(config.runnerPublicKey === pin.publicKey, 'MERGE_ARCH_RUNNER_PIN', 'Original native runner authority differs from its independent pin');
|
|
135
|
+
const assertCurrent = async () => { assertReceipt(); await assertNativeCurrent(); };
|
|
136
|
+
await assertCurrent(); return action(assertCurrent);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { snapshotAuthority14 } from './local-mutation-snapshot.mjs';
|
|
5
|
+
import { canonical, sha256, fail, identifier, paths, noLinks, MISSING_SHA256, context, withMergeFence, directory, read, json, writeJson } from './local-merge-storage.mjs';
|
|
6
|
+
import { inspectMergeLease, signMergeReceipt } from './local-merge-authority.mjs';
|
|
7
|
+
import { assertDistinctMergeTargets } from './local-merge-targets.mjs';
|
|
8
|
+
|
|
9
|
+
export const mergeArtifactDigest = files => sha256(`validator.file-manifest/1.0\n${files.map(file => `${file.path}\0${file.sha256}`).join('\n')}`);
|
|
10
|
+
const entry = (name, value) => ({ path: name, exists: value.exists, sha256: value.sha256, mode: value.mode });
|
|
11
|
+
export async function readMergeSnapshot(ctx, receipt) {
|
|
12
|
+
const value = await snapshotAuthority14(ctx.input, ctx.root, receipt);
|
|
13
|
+
return { manifest: value.manifest, directory: value.snapshotRoot };
|
|
14
|
+
}
|
|
15
|
+
async function freeze(ctx, role, root, names, mappings = []) {
|
|
16
|
+
const artifactId = `${role}-${randomUUID()}`, directoryPath = path.join(ctx.state, 'artifacts', artifactId);
|
|
17
|
+
await directory(path.join(directoryPath, 'blobs')); const files = []; let total = 0;
|
|
18
|
+
for (const name of names) {
|
|
19
|
+
const sourcePath = role === 'source' ? mappings.find(item => item.targetPath === name).sourcePath : name;
|
|
20
|
+
const value = await read(path.join(root, sourcePath), ctx.input, sourcePath, role === 'target');
|
|
21
|
+
total += value.bytes?.length ?? 0; if (total > 64 * 1024 * 1024) fail('MERGE_ARTIFACT_LIMIT', 'Merge artifact exceeds its bounded byte quota');
|
|
22
|
+
if (value.exists) {
|
|
23
|
+
const blob = path.join(directoryPath, 'blobs', value.sha256);
|
|
24
|
+
try { await fs.writeFile(blob, value.bytes, { flag: 'wx', mode: 0o600 }); }
|
|
25
|
+
catch (error) { if (error.code !== 'EEXIST') throw error;
|
|
26
|
+
if (!(await read(blob, ctx.input, sourcePath)).bytes.equals(value.bytes)) fail('MERGE_BLOB_CHANGED', 'Duplicate artifact blob differs from its actual bytes'); }
|
|
27
|
+
}
|
|
28
|
+
files.push({ ...entry(name, value), ...(role === 'source' ? { sourcePath } : {}) });
|
|
29
|
+
}
|
|
30
|
+
const manifest = { schemaVersion: 'entrorove.merge-artifact/1.0', artifactId, role, repositoryDigest: ctx.repo, flowId: ctx.flowId, stepId: ctx.stepId, files };
|
|
31
|
+
await writeJson(path.join(directoryPath, 'manifest.json'), manifest);
|
|
32
|
+
return { ref: { kind: 'snapshot', artifactId, manifestDigest: sha256(canonical(manifest)) }, manifest };
|
|
33
|
+
}
|
|
34
|
+
export async function readFrozenMergeArtifact(input, ref) {
|
|
35
|
+
const ctx = await context(input);
|
|
36
|
+
if (!ref || ref.kind !== 'snapshot' || !/^[0-9a-f]{64}$/.test(ref.manifestDigest ?? '')) fail('MERGE_REF_INVALID', 'Merge refs must identify frozen private snapshot manifests');
|
|
37
|
+
const directoryPath = path.join(ctx.state, 'artifacts', identifier(ref.artifactId)), manifest = await json(path.join(directoryPath, 'manifest.json'));
|
|
38
|
+
if (Object.keys(ref).sort().join('|') !== 'artifactId|kind|manifestDigest'
|
|
39
|
+
|| Object.keys(manifest).sort().join('|') !== 'artifactId|files|flowId|repositoryDigest|role|schemaVersion|stepId'
|
|
40
|
+
|| !['source', 'target'].includes(manifest.role) || manifest.schemaVersion !== 'entrorove.merge-artifact/1.0' || manifest.artifactId !== ref.artifactId || manifest.repositoryDigest !== ctx.repo
|
|
41
|
+
|| manifest.flowId !== ctx.flowId || manifest.stepId !== ctx.stepId || sha256(canonical(manifest)) !== ref.manifestDigest
|
|
42
|
+
|| canonical(paths(manifest.files.map(file => file.path))) !== canonical(manifest.files.map(file => file.path))) fail('MERGE_REF_CHANGED', 'Frozen merge manifest or reference changed');
|
|
43
|
+
const files = [];
|
|
44
|
+
for (const file of manifest.files) {
|
|
45
|
+
if (manifest.role === 'source') paths([file.sourcePath]);
|
|
46
|
+
if (Object.keys(file).sort().join('|') !== (manifest.role === 'source' ? 'exists|mode|path|sha256|sourcePath' : 'exists|mode|path|sha256')
|
|
47
|
+
|| typeof file.exists !== 'boolean' || !/^[0-9a-f]{64}$/.test(file.sha256 ?? '')
|
|
48
|
+
|| file.exists && (!Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777)
|
|
49
|
+
|| !file.exists && (file.sha256 !== MISSING_SHA256 || file.mode !== null)
|
|
50
|
+
|| manifest.role === 'source' && !file.exists) fail('MERGE_REF_CHANGED', 'Frozen merge file metadata is invalid');
|
|
51
|
+
const value = file.exists ? await read(path.join(directoryPath, 'blobs', file.sha256), ctx.input, file.sourcePath ?? file.path) : { bytes: null };
|
|
52
|
+
if (file.exists && value.sha256 !== file.sha256) fail('MERGE_BLOB_CHANGED', 'Frozen merge blob differs from its signed actual digest');
|
|
53
|
+
files.push({ ...file, bytes: value.bytes });
|
|
54
|
+
}
|
|
55
|
+
return { manifest, files };
|
|
56
|
+
}
|
|
57
|
+
export async function prepareMergeArtifacts(input) {
|
|
58
|
+
const ctx = await context(input);
|
|
59
|
+
return withMergeFence(ctx, async (state, registration) => {
|
|
60
|
+
const lease = inspectMergeLease(input.mergeLease, ctx, state, registration);
|
|
61
|
+
const snapshot = await readMergeSnapshot(ctx, input.snapshotReceipt), scopePaths = paths(snapshot.manifest.paths), names = paths(lease.mappings.map(item => item.targetPath));
|
|
62
|
+
if (canonical(scopePaths) !== canonical(lease.scopePaths)
|
|
63
|
+
|| snapshot.manifest.contractDigest !== lease.architectureContractDigest) fail('MERGE_SCOPE_CHANGED', 'Frozen merge scope and architecture must exactly match its original signed lease and snapshot');
|
|
64
|
+
const suppliedSourceRoot = input.sourceRoot ?? ctx.root; await noLinks(suppliedSourceRoot); const sourceRoot = await fs.realpath(suppliedSourceRoot);
|
|
65
|
+
if (sourceRoot !== ctx.root) fail('MERGE_SOURCE_ORIGIN', 'Merge candidate origins must retain the original registered repository');
|
|
66
|
+
await assertDistinctMergeTargets(ctx.input, scopePaths.map(path => ({ path })));
|
|
67
|
+
const target = await freeze(ctx, 'target', ctx.root, scopePaths), source = await freeze(ctx, 'source', sourceRoot, names, lease.mappings);
|
|
68
|
+
if (canonical(target.manifest.files) !== canonical(snapshot.manifest.files)) fail('MERGE_TARGET_CHANGED', 'Merge target changed from its frozen original snapshot');
|
|
69
|
+
const targetFiles = target.manifest.files.filter(file => names.includes(file.path));
|
|
70
|
+
const changed = names.filter((name, index) => { const { sourcePath, ...file } = source.manifest.files[index]; return canonical(file) !== canonical(targetFiles[index]); });
|
|
71
|
+
if (!changed.length) fail('MERGE_CHANGES_REQUIRED', 'Merge requires nonempty actual file changes');
|
|
72
|
+
const sourceFiles = source.manifest.files.map(({ exists, ...file }) => file);
|
|
73
|
+
if (sourceFiles.some(file => !lease.mappingProof.entries.some(entry => entry.sourcePath === file.sourcePath && entry.targetPath === file.path && entry.sha256 === file.sha256))) {
|
|
74
|
+
fail('MERGE_SOURCE_ANCESTRY', 'Actual candidate source differs from its frozen native producer mapping proof');
|
|
75
|
+
}
|
|
76
|
+
return signMergeReceipt(ctx.input, { schemaVersion: 'entrorove.merge-preparation-receipt/1.0', repositoryDigest: ctx.repo,
|
|
77
|
+
flowId: ctx.flowId, stepId: ctx.stepId, baselineRef: lease.baselineRef, sourceRef: source.ref, targetRef: target.ref, paths: changed,
|
|
78
|
+
...Object.fromEntries(['mergeId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch'].map(field => [field, lease[field]])),
|
|
79
|
+
mergeLeaseDigest: sha256(canonical(lease)), architectureLedgerDigest: lease.architectureLedgerDigest,
|
|
80
|
+
scopePaths,
|
|
81
|
+
mappings: lease.mappings, mappingDigest: lease.mappingDigest, mappingProof: lease.mappingProof,
|
|
82
|
+
sourceFiles, targetFiles, mergedArtifactSha256: mergeArtifactDigest(sourceFiles), snapshotDigest: input.snapshotReceipt.snapshotDigest,
|
|
83
|
+
afterManifest: sourceFiles.map(({ path, sha256 }) => ({ path, sha256 })), beforeFullManifest: target.manifest.files,
|
|
84
|
+
snapshotReceiptDigest: input.snapshotReceipt.receiptDigest, architectureContractDigest: input.snapshotReceipt.contractDigest });
|
|
85
|
+
});
|
|
86
|
+
}
|