@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,26 @@
1
+ import path from 'node:path';
2
+ import { verifySnapshotManifest } from './local-integrity.mjs';
3
+ import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
4
+ import { context, identifier, json, read, fail } from './local-merge-storage.mjs';
5
+
6
+ export async function readSnapshotRollbackImages(input) {
7
+ const ctx = await context({ ...input, stepId: input.stepId ?? 'snapshot-rollback' });
8
+ const directory = path.join(ctx.home, '.entrorove', 'snapshots', ctx.repo, ctx.flowId, identifier(input.snapshotId));
9
+ const manifest = await json(path.join(directory, 'manifest.json'));
10
+ const check = verifySnapshotManifest(manifest, { snapshotId: input.snapshotId, flowId: ctx.flowId,
11
+ stepId: input.stepId ?? manifest.stepId, repositoryDigest: ctx.repo });
12
+ if (!check.verified) fail('MERGE_SNAPSHOT_CORRUPT', check.reason);
13
+ ctx.stepId = manifest.stepId; ctx.input.stepId = manifest.stepId; ctx.input.paths = manifest.paths;
14
+ const before = [], after = [];
15
+ for (const file of manifest.files) {
16
+ const copy = await readSnapshotCopy14(ctx.input, directory, manifest, file.path);
17
+ const value = { sha256: copy.sha256, bytes: copy.content };
18
+ if (value.sha256 !== file.sha256) fail('MERGE_SNAPSHOT_CORRUPT', 'Every original snapshot backup must match before restoration starts');
19
+ before.push({ path: file.path, ...await read(path.join(ctx.root, file.path), ctx.input, file.path, true) });
20
+ after.push({ ...file, bytes: value.bytes });
21
+ }
22
+ return { ctx, manifest, before, after };
23
+ }
24
+ export async function atomicSnapshotRollback(input) {
25
+ fail('MERGE_ROLLBACK_AUTHORITY_REQUIRED', 'Legacy snapshot hashes do not authorize restoration; use the original durable merge transaction and its fresh cloud-signed rollback authorization');
26
+ }
@@ -0,0 +1,103 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants, promises as fs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { canonical, sha256 } from './local-integrity.mjs';
6
+ import { readBudgetedProjectFile, budgetBindingForRead } from './local-budget-reads.mjs';
7
+ import { withExclusiveLock } from './local-archguard-state.mjs';
8
+ import { context as handoffContext, readRegistry } from './local-task-handoff-storage.mjs';
9
+ import { readTaskHandoffFence, assertTaskHandoffActive } from './local-task-handoff-fence.mjs';
10
+
11
+ export { canonical, sha256 };
12
+ export const MISSING_SHA256 = sha256('entrorove.missing-file/1.0');
13
+ export function fail(code, message) { throw Object.assign(new Error(message), { code }); }
14
+ export function identifier(value) {
15
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) fail('MERGE_ID_INVALID', 'Merge identifier is invalid');
16
+ return value;
17
+ }
18
+ export function safePath(value) {
19
+ if (typeof value !== 'string' || !value || value.length > 500 || value !== value.trim() || value !== value.normalize('NFC') || path.isAbsolute(value)
20
+ || /^[~]/.test(value) || /[:\\\u0000-\u001f\u007f]/.test(value)
21
+ || value.split('/').some(part => !part || part === '.' || part === '..')) fail('MERGE_PATH_INVALID', 'Merge path is unsafe');
22
+ return value;
23
+ }
24
+ export function paths(value) {
25
+ if (!Array.isArray(value) || !value.length || value.length > 512) fail('MERGE_PATHS_REQUIRED', 'Merge paths must be nonempty and bounded');
26
+ const sorted = value.map(safePath).sort();
27
+ if (new Set(sorted).size !== sorted.length || sorted.some((item, index) => sorted.slice(index + 1).some(other => other.startsWith(`${item}/`)))) {
28
+ fail('MERGE_PATHS_OVERLAP', 'Merge paths must be unique and cannot overlap');
29
+ }
30
+ return sorted;
31
+ }
32
+ export async function noLinks(file, missing = false) {
33
+ let current = path.parse(path.resolve(file)).root;
34
+ for (const part of path.resolve(file).slice(current.length).split(path.sep)) {
35
+ current = path.join(current, part);
36
+ try { const stat = await fs.lstat(current); if (stat.isSymbolicLink()) fail('MERGE_SYMLINK_REJECTED', 'Merge paths and private state forbid symbolic links'); }
37
+ catch (error) { if (missing && error.code === 'ENOENT') return; throw error; }
38
+ }
39
+ }
40
+ export async function directory(file) {
41
+ await noLinks(file, true); await fs.mkdir(file, { recursive: true, mode: 0o700 }); await noLinks(file);
42
+ if (!(await fs.lstat(file)).isDirectory()) fail('MERGE_STATE_INVALID', 'Merge directory must be real');
43
+ }
44
+ export async function read(file, input = null, sourcePath = null, missing = false) {
45
+ await noLinks(file, missing); let handle;
46
+ try {
47
+ handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
48
+ const before = await handle.stat();
49
+ if (!before.isFile() || before.size > 32 * 1024 * 1024) fail('MERGE_FILE_INVALID', 'Merge input must be a bounded regular file');
50
+ const bytes = input ? await budgetedSource(input, file, sourcePath) : await handle.readFile();
51
+ const after = await handle.stat(), named = await fs.lstat(file);
52
+ if (before.ino !== named.ino || before.dev !== named.dev || before.size !== after.size || before.size !== named.size || before.mtimeMs !== after.mtimeMs
53
+ || before.mtimeMs !== named.mtimeMs || before.mode !== after.mode || before.mode !== named.mode
54
+ || bytes.length !== before.size || named.isSymbolicLink()) fail('MERGE_READ_RACE', 'Merge input changed while reading');
55
+ return { exists: true, sha256: sha256(bytes), mode: before.mode & 0o777, bytes,
56
+ fileId: `${before.dev}:${before.ino}`, nlink: before.nlink };
57
+ } catch (error) { if (missing && error.code === 'ENOENT') return { exists: false, sha256: MISSING_SHA256, mode: null, bytes: null }; throw error; }
58
+ finally { await handle?.close(); }
59
+ }
60
+ async function budgetedSource(input, file, sourcePath) {
61
+ if (!await budgetBindingForRead(input) || path.resolve(file) === path.join(input.repositoryRoot, safePath(sourcePath))) {
62
+ return readBudgetedProjectFile(input, file, { sourcePath });
63
+ }
64
+ const proxy = path.join(input.home, '.entrorove', 'snapshots', sha256(input.repositoryRoot).slice(0, 32), input.flowId, `merge-read-${randomUUID()}`);
65
+ await directory(path.dirname(proxy)); await fs.mkdir(proxy, { mode: 0o700 });
66
+ const target = path.join(proxy, safePath(sourcePath));
67
+ try { await directory(path.dirname(target)); await fs.link(file, target);
68
+ return await readBudgetedProjectFile(input, target, { sourcePath }); }
69
+ finally { await fs.rm(proxy, { recursive: true, force: true }); }
70
+ }
71
+ export async function atomic(file, bytes, mode = 0o600, beforePublish = null) {
72
+ await noLinks(file, true); await directory(path.dirname(file));
73
+ const temporary = `${file}.${randomUUID()}.tmp`; const handle = await fs.open(temporary, 'wx', mode);
74
+ try { await handle.writeFile(bytes); await handle.chmod(mode); await handle.sync(); } finally { await handle.close(); }
75
+ try { await noLinks(file, true); if (beforePublish) await beforePublish(); await fs.rename(temporary, file); }
76
+ finally { await fs.rm(temporary, { force: true }); }
77
+ }
78
+ export const json = async file => JSON.parse((await read(file)).bytes.toString('utf8'));
79
+ export const writeJson = (file, value) => atomic(file, canonical(value) + '\n');
80
+ export async function context(input) {
81
+ if (typeof input.repositoryRoot !== 'string' || !path.isAbsolute(input.repositoryRoot)) fail('MERGE_ROOT_INVALID', 'Merge repository root must be absolute');
82
+ await noLinks(input.repositoryRoot); const root = await fs.realpath(input.repositoryRoot); await noLinks(root);
83
+ if (!(await fs.stat(root)).isDirectory()) fail('MERGE_ROOT_INVALID', 'Merge repository root must be a real directory');
84
+ const suppliedHome = input.home ?? os.homedir(); await noLinks(suppliedHome);
85
+ if (typeof suppliedHome !== 'string' || !path.isAbsolute(suppliedHome)) fail('MERGE_HOME_INVALID', 'Merge home must be an absolute private directory');
86
+ const home = await fs.realpath(suppliedHome); await noLinks(home);
87
+ const repo = sha256(root).slice(0, 32), flowId = identifier(input.flowId), stepId = identifier(input.stepId);
88
+ const state = path.join(home, '.entrorove', 'merges', repo, flowId); await directory(state);
89
+ for (const file of [path.join(home, '.entrorove'), path.join(home, '.entrorove', 'merges'), path.dirname(state), state]) {
90
+ const stat = await fs.lstat(file);
91
+ if (process.platform !== 'win32' && stat.mode & 0o077 || typeof process.getuid === 'function' && stat.uid !== process.getuid()) fail('MERGE_STATE_PERMISSIONS', 'Merge private hierarchy must remain privately owned');
92
+ }
93
+ return { root, home, repo, flowId, stepId, state, input: { ...input, repositoryRoot: root, home } };
94
+ }
95
+ export async function withMergeFence(ctx, action) {
96
+ const fence = await handoffContext(ctx.input, true);
97
+ return withExclusiveLock(`${fence.file}.lock`, async () => {
98
+ const state = await readTaskHandoffFence(ctx.input);
99
+ await assertTaskHandoffActive({ ...ctx.input, epoch: state.epoch });
100
+ const registration = (await readRegistry(fence.registry)).bindings[fence.registryKey];
101
+ return withExclusiveLock(path.join(ctx.home, '.entrorove', 'merges', ctx.repo, 'repository.lock'), () => action(state, registration));
102
+ });
103
+ }
@@ -0,0 +1,47 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fail, noLinks, paths, context, directory } from './local-merge-storage.mjs';
5
+
6
+ async function assertFileIdentities(input, names) {
7
+ const identities = new Set();
8
+ for (const name of names) {
9
+ const file = path.join(input.repositoryRoot, name); await noLinks(file, true);
10
+ try {
11
+ const stat = await fs.lstat(file), identity = `${stat.dev}:${stat.ino}`;
12
+ if (stat.isFile() && (stat.nlink !== 1 || identities.has(identity))) {
13
+ fail('MERGE_TARGET_ALIAS', 'Merge targets must be distinct files without shared hard-link identities');
14
+ }
15
+ if (stat.isFile()) identities.add(identity);
16
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
17
+ }
18
+ }
19
+ async function assertFilesystemNames(input, names) {
20
+ const root = input.repositoryRoot; await noLinks(root);
21
+ const original = await fs.lstat(root), ctx = await context(input), privateRoot = path.join(ctx.state, 'filesystem-probes');
22
+ await directory(privateRoot);
23
+ if ((await fs.lstat(privateRoot)).dev !== original.dev) fail('MERGE_TARGET_FILESYSTEM_MISMATCH', 'Private alias validation must use the actual target filesystem device');
24
+ const probe = path.join(privateRoot, randomUUID());
25
+ await fs.mkdir(probe, { mode: 0o700 }); const owner = await fs.lstat(probe);
26
+ try {
27
+ for (const name of names) {
28
+ const file = path.join(probe, name); await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
29
+ try { const handle = await fs.open(file, 'wx', 0o600); await handle.close(); }
30
+ catch (error) {
31
+ if (['EEXIST', 'EISDIR', 'ENOTDIR'].includes(error.code)) fail('MERGE_TARGET_ALIAS', 'Target filesystem maps different batch paths to an overlapping name');
32
+ throw error;
33
+ }
34
+ }
35
+ const current = await fs.lstat(root);
36
+ if (current.ino !== original.ino || current.dev !== original.dev || current.isSymbolicLink()) fail('MERGE_TARGET_CAS', 'Target repository changed during filesystem alias validation');
37
+ } finally {
38
+ const current = await fs.lstat(probe);
39
+ if (current.ino !== owner.ino || current.dev !== owner.dev || current.isSymbolicLink()) fail('MERGE_TARGET_CAS', 'Owned filesystem probe changed before cleanup');
40
+ await fs.rm(probe, { recursive: true });
41
+ }
42
+ }
43
+ export async function assertDistinctMergeTargets(input, files) {
44
+ const names = paths(files.map(file => file.path));
45
+ await assertFileIdentities(input, names); await assertFilesystemNames(input, names);
46
+ await assertFileIdentities(input, names);
47
+ }
@@ -0,0 +1,133 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { canonical, sha256, fail, paths, MISSING_SHA256, read, atomic, noLinks } from './local-merge-storage.mjs';
4
+ import { assertDistinctMergeTargets } from './local-merge-targets.mjs';
5
+
6
+ export const metadata = file => ({ path: file.path, exists: file.exists, sha256: file.sha256, mode: file.mode });
7
+ function validate(files) {
8
+ if (canonical(paths(files.map(file => file.path))) !== canonical(files.map(file => file.path))) fail('MERGE_BATCH_PATHS', 'Atomic batch paths must be sorted, unique and nonoverlapping');
9
+ for (const file of files) {
10
+ if (typeof file.exists !== 'boolean' || file.exists && (!Buffer.isBuffer(file.bytes) || sha256(file.bytes) !== file.sha256
11
+ || !Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777)
12
+ || !file.exists && (file.bytes !== null || file.sha256 !== MISSING_SHA256 || file.mode !== null)) {
13
+ fail('MERGE_BATCH_BACKUP', 'Every atomic batch image must have its original existence, bytes, mode and digest');
14
+ }
15
+ }
16
+ }
17
+ async function current(input, image) {
18
+ const value = await read(path.join(input.repositoryRoot, image.path), input, image.path, true);
19
+ return { path: image.path, ...value };
20
+ }
21
+ export async function assertBatchImage(input, files) {
22
+ validate(files);
23
+ for (const file of files) {
24
+ if (canonical(metadata(await current(input, file))) !== canonical(metadata(file))) fail('MERGE_TARGET_CAS', 'A target differs from its actual frozen batch image');
25
+ }
26
+ }
27
+ async function createParents(input, file, parents) {
28
+ let currentPath = input.repositoryRoot;
29
+ for (const part of path.relative(input.repositoryRoot, path.dirname(file)).split(path.sep).filter(Boolean)) {
30
+ currentPath = path.join(currentPath, part); await noLinks(currentPath, true);
31
+ try { await fs.mkdir(currentPath, { mode: 0o700 });
32
+ const stat = await fs.lstat(currentPath); parents.push({ path: currentPath, ino: stat.ino, dev: stat.dev }); }
33
+ catch (error) { if (error.code !== 'EEXIST' || !(await fs.lstat(currentPath)).isDirectory()) throw error; }
34
+ }
35
+ }
36
+ async function cleanupParents(parents) {
37
+ const failures = [];
38
+ for (const entry of [...parents].reverse()) {
39
+ try { const stat = await fs.lstat(entry.path);
40
+ if (stat.isSymbolicLink() || stat.ino !== entry.ino || stat.dev !== entry.dev) fail('MERGE_RECOVERY_CAS', 'Created parent directory changed before restoration');
41
+ await fs.rmdir(entry.path); }
42
+ catch (error) { if (error.code !== 'ENOENT') failures.push({ path: entry.path, code: error.code ?? 'MERGE_PARENT_RESTORE_IO', message: error.message }); }
43
+ }
44
+ return failures;
45
+ }
46
+ async function writeImage(input, image, parents = [], expected = null) {
47
+ const file = path.join(input.repositoryRoot, image.path); await noLinks(file, true);
48
+ if (image.exists) { await createParents(input, file, parents);
49
+ await atomic(file, image.bytes, image.mode, expected ? () => assertBatchImage(input, [expected]) : null); }
50
+ else { if (expected) await assertBatchImage(input, [expected]);
51
+ try { await fs.unlink(file); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
52
+ }
53
+ async function restoreTouched(input, before, after, touched) {
54
+ const errors = [];
55
+ for (const index of [...touched].reverse()) {
56
+ try {
57
+ const actual = metadata(await current(input, before[index]));
58
+ if (canonical(actual) === canonical(metadata(before[index]))) continue;
59
+ if (canonical(actual) !== canonical(metadata(after[index]))) fail('MERGE_RECOVERY_CAS', 'Recovery refuses to overwrite an unknown third-party target');
60
+ await writeImage(input, before[index], [], after[index]);
61
+ } catch (error) { errors.push({ path: before[index].path, code: error.code ?? 'MERGE_RESTORE_IO', message: error.message }); }
62
+ }
63
+ return errors;
64
+ }
65
+ async function restorationFailures(input, before) {
66
+ try { await assertBatchImage(input, before); return []; }
67
+ catch (error) { return [{ path: 'batch', code: error.code ?? 'MERGE_RESTORATION_INCOMPLETE', message: error.message }]; }
68
+ }
69
+ export async function applyFileBatch(input, before, after, effects = {}) {
70
+ validate(before); validate(after);
71
+ if (canonical(before.map(file => file.path)) !== canonical(after.map(file => file.path))) fail('MERGE_BATCH_SCOPE', 'Atomic before/after scopes differ');
72
+ const changed = before.map((file, index) => canonical(metadata(file)) !== canonical(metadata(after[index])) ? index : -1).filter(index => index >= 0);
73
+ if (!changed.length) fail('MERGE_CHANGES_REQUIRED', 'An atomic merge or rollback cannot be an empty change');
74
+ await assertDistinctMergeTargets(input, before);
75
+ await assertBatchImage(input, before);
76
+ const touched = [], createdParents = [];
77
+ try {
78
+ if (effects.prepare) await effects.prepare();
79
+ for (const index of changed) {
80
+ await assertBatchImage(input, [before[index]]); touched.push(index);
81
+ if (after[index].exists) await createParents(input, path.join(input.repositoryRoot, after[index].path), createdParents);
82
+ if (effects.progress) await effects.progress({ touched: [...touched], createdParents: [...createdParents] });
83
+ await assertBatchImage(input, [before[index]]);
84
+ await writeImage(input, after[index], createdParents, before[index]);
85
+ }
86
+ await assertBatchImage(input, after);
87
+ const result = effects.finish ? await effects.finish() : null;
88
+ return { applied: true, before: before.map(metadata), after: after.map(metadata), createdParents, result };
89
+ } catch (error) {
90
+ const failures = [...await restoreTouched(input, before, after, touched), ...await cleanupParents(createdParents), ...await restorationFailures(input, before)];
91
+ const outcome = { applied: false, code: error.code ?? 'MERGE_BATCH_IO', reason: error.message,
92
+ rollbackOutcome: failures.length ? 'restoration-failed' : 'restored', recoveryRequired: failures.length > 0, touched, createdParents, failures };
93
+ if (effects.failed) {
94
+ try { await effects.failed(outcome); }
95
+ catch (auditError) { outcome.auditFailure = { code: auditError.code ?? 'MERGE_AUDIT_IO', reason: auditError.message }; outcome.recoveryRequired = true; }
96
+ }
97
+ return outcome;
98
+ }
99
+ }
100
+ export async function recoverFileBatch(input, before, after, touched, createdParents = []) {
101
+ validate(before); validate(after);
102
+ if (!Array.isArray(touched) || touched.some(index => !Number.isSafeInteger(index) || index < 0 || index >= before.length)
103
+ || new Set(touched).size !== touched.length || canonical(before.map(file => file.path)) !== canonical(after.map(file => file.path))) fail('MERGE_RECOVERY_INVALID', 'Recovery requires the exact original batch journal');
104
+ if (!Array.isArray(createdParents) || createdParents.some(entry => typeof entry.path !== 'string'
105
+ || path.relative(input.repositoryRoot, entry.path).startsWith('..') || !Number.isSafeInteger(entry.ino) || !Number.isSafeInteger(entry.dev))) fail('MERGE_RECOVERY_INVALID', 'Recovery directory ownership is invalid');
106
+ const failures = [...await restoreTouched(input, before, after, touched), ...await cleanupParents(createdParents), ...await restorationFailures(input, before)];
107
+ return { restored: failures.length === 0, rollbackOutcome: failures.length ? 'restoration-failed' : 'restored', failures };
108
+ }
109
+ export async function rollbackFileBatch(input, before, after, createdParents, effects = {}) {
110
+ const original = [];
111
+ for (const entry of createdParents) {
112
+ if (typeof entry.path !== 'string' || path.relative(input.repositoryRoot, entry.path).startsWith('..')
113
+ || entry.path === input.repositoryRoot) fail('MERGE_PARENT_CHANGED', 'Rollback parent is outside its original repository');
114
+ await noLinks(entry.path); const stat = await fs.lstat(entry.path);
115
+ if (!stat.isDirectory() || stat.ino !== entry.ino || stat.dev !== entry.dev) fail('MERGE_PARENT_CHANGED', 'Rollback parent lost its original created-directory identity');
116
+ original.push({ ...entry, mode: stat.mode & 0o777 });
117
+ }
118
+ const outcome = await applyFileBatch(input, before, after, { ...effects, finish: async () => {
119
+ const failures = await cleanupParents(original);
120
+ if (failures.length) fail('MERGE_PARENT_RESTORE_IO', 'Rollback cannot remove a changed or nonempty created parent');
121
+ return effects.finish ? effects.finish() : null;
122
+ } });
123
+ if (!outcome.applied) {
124
+ outcome.restoredParents = [];
125
+ for (const entry of original) {
126
+ try { const stat = await fs.lstat(entry.path);
127
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== entry.mode) fail('MERGE_PARENT_RESTORE_IO', 'Rollback failure did not retain its original parent mode and existence');
128
+ outcome.restoredParents.push({ path: entry.path, ino: stat.ino, dev: stat.dev }); }
129
+ catch (error) { outcome.failures.push({ path: entry.path, code: error.code, message: error.message }); outcome.recoveryRequired = true; outcome.rollbackOutcome = 'restoration-failed'; }
130
+ }
131
+ }
132
+ return outcome;
133
+ }
@@ -0,0 +1,34 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { canonical, sha256 } from './local-integrity.mjs';
4
+ import { mutationRoot14, mutationStateRoot14, mutationTarget14, privateMutationDirectory14 } from './local-mutation-files.mjs';
5
+
6
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
+ const CODE = /^[A-Z][A-Z0-9_]{0,127}$/;
8
+ function safeId(value) { return typeof value === 'string' && ID.test(value) ? value : null; }
9
+ export async function auditMutation14(input, event, fields = {}) {
10
+ const root = await mutationRoot14(input.repositoryRoot); const stateRoot = await mutationStateRoot14(input);
11
+ await privateMutationDirectory14(stateRoot, { privateFrom: stateRoot });
12
+ const file = await mutationTarget14(stateRoot, 'invocations.jsonl', true);
13
+ const handle = await fs.open(file.target, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0), 0o600);
14
+ try {
15
+ const opened = await handle.stat(); const current = await mutationTarget14(stateRoot, 'invocations.jsonl');
16
+ if (!opened.isFile() || opened.dev !== current.stat.dev || opened.ino !== current.stat.ino) throw new Error('Mutation audit file changed during open');
17
+ const record = { schemaVersion: 'entrorove.local-execution/1.0', at: new Date().toISOString(), event,
18
+ repositoryDigest: sha256(root).slice(0, 32), flowId: safeId(input.flowId), stepId: safeId(input.stepId),
19
+ ...(typeof input.targetPath === 'string' ? { targetPathDigest: sha256(input.targetPath) } : {}), ...fields };
20
+ await handle.write(`${canonical(record)}\n`); await handle.sync();
21
+ if (process.platform !== 'win32') await handle.chmod(0o600);
22
+ const after = await mutationTarget14(stateRoot, path.basename(file.target));
23
+ if (after.stat.dev !== opened.dev || after.stat.ino !== opened.ino) throw new Error('Mutation audit file changed during append');
24
+ } finally { await handle.close(); }
25
+ }
26
+ export async function recordMutationDenied14(input, cause, operation = 'guarded-write') {
27
+ const code = typeof cause?.code === 'string' && CODE.test(cause.code) ? cause.code : 'ENTROROVE_MUTATION_REJECTED';
28
+ try { await auditMutation14(input, `${operation}-rejected`, { code }); }
29
+ catch { throw Object.assign(new Error('Mutation was refused and its audit could not be persisted safely'), { code: 'ENTROROVE_MUTATION_AUDIT_FAILED' }); }
30
+ }
31
+ export async function withMutationAudit14(input, operation, action) {
32
+ try { return await action(); }
33
+ catch (cause) { await recordMutationDenied14(input, cause, operation); throw cause; }
34
+ }
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { appendLedgerRecord } from './local-ledger.mjs';
4
+ import { withStateLock } from './local-state-lock.mjs';
5
+ import { withTaskHandoffIssueFence, withTaskHandoffWriteFence, assertTaskHandoffSnapshotBinding } from './local-task-handoff-fence.mjs';
6
+ import { mutationFail14, mutationPath14, mutationStateRoot14,
7
+ privateMutationDirectory14, writeMutationProjectFile14 } from './local-mutation-files.mjs';
8
+ import { mutationPass14, mutationLifetime14 } from './local-mutation-wire.mjs';
9
+ import { mutationOriginalBinding14, mutationSnapshotBinding14 } from './local-mutation-binding.mjs';
10
+ import { installMutationPass14, mutationPassLocations14, readMutationConsumption14,
11
+ readStoredMutationPass14, saveMutationConsumption14 } from './local-mutation-pass-store.mjs';
12
+ import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
13
+ import { withSwarmMutationLease } from './local-swarm-mutation.mjs';
14
+ export { beginMutationBatch14, finishMutationBatch14, failMutationBatch14 } from './local-mutation-batch.mjs';
15
+
16
+ const SCHEMA = 'entrorove.local-execution/1.0';
17
+ function suppliedPass(input) {
18
+ if (!input.mutationPass) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_REQUIRED', 'Mutation requires its original short-lived cloud-signed pass');
19
+ return mutationPass14(input.mutationPass);
20
+ }
21
+ export async function mutateGate14(input) {
22
+ return withMutationAudit14(input, 'mutate-gate', async () => {
23
+ const pass = suppliedPass(input), binding = await mutationOriginalBinding14(input, pass);
24
+ const snapshot = await mutationSnapshotBinding14(input, pass, { allSources: true });
25
+ assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
26
+ return { schemaVersion: SCHEMA, status: 'verified', allowed: true, snapshotId: snapshot.manifest.snapshotId,
27
+ receiptDigest: pass.snapshotReceiptDigest, paths: [...pass.paths], coordinationRequired: false };
28
+ });
29
+ }
30
+ export async function issueMutationPass14(input) {
31
+ return withMutationAudit14(input, 'mutation-pass-issue', async () => {
32
+ const pass = suppliedPass(input);
33
+ return withTaskHandoffIssueFence(input, async state => {
34
+ const binding = await mutationOriginalBinding14(input, pass, state);
35
+ await mutationSnapshotBinding14(input, pass, { allSources: true });
36
+ assertTaskHandoffSnapshotBinding(state, pass.snapshotReceipt);
37
+ mutationLifetime14(pass);
38
+ const locations = await installMutationPass14(input, binding, pass);
39
+ await auditMutation14(input, 'mutation-pass-issued', { passId: pass.passId, leaseId: pass.leaseId,
40
+ generation: pass.generation, executionAttemptNonce: pass.executionAttemptNonce });
41
+ return { schemaVersion: SCHEMA, gatePassPath: locations.gatePassPath, pass };
42
+ });
43
+ });
44
+ }
45
+ async function checkedPass(input, state) {
46
+ const pass = await readStoredMutationPass14(input), target = mutationPath14(input.targetPath, 'targetPath');
47
+ if (!pass.paths.includes(target)) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Guarded writing is outside the cloud pass path set');
48
+ const binding = await mutationOriginalBinding14(input, pass, state);
49
+ const snapshot = await mutationSnapshotBinding14(input, pass, { targetPath: target });
50
+ assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
51
+ const locations = await mutationPassLocations14(input, binding, pass);
52
+ const ledger = await readMutationConsumption14(pass, locations);
53
+ if (Object.hasOwn(ledger.writes, target)) mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Mutation pass was already consumed or has an unresolved physical write');
54
+ return { pass, target, binding, snapshot, locations, ledger };
55
+ }
56
+ export async function verifyMutationPass14(input) {
57
+ return withMutationAudit14(input, 'mutation-pass-verify', async () => {
58
+ const value = await checkedPass(input);
59
+ return { schemaVersion: SCHEMA, verified: true, pass: value.pass, targetPath: value.target };
60
+ });
61
+ }
62
+ function contentFor(input, baseline) {
63
+ if (input.expectedSha256 !== undefined && input.expectedSha256 !== null && input.expectedSha256 !== baseline.sha256) {
64
+ mutationFail14('ENTROROVE_WRITE_BASELINE', 'Target differs from the expected snapshot hash');
65
+ }
66
+ let content; try { content = typeof input.content === 'string' ? input.content : JSON.stringify(input.content); }
67
+ catch { mutationFail14('ENTROROVE_CONTENT_INVALID', 'Guarded-write content must be finite JSON or text'); }
68
+ if (typeof content !== 'string') mutationFail14('ENTROROVE_CONTENT_INVALID', 'Guarded-write content must be JSON or text');
69
+ return content;
70
+ }
71
+ async function performWrite(input, state, value) {
72
+ const current = await checkedPass(input, state), { pass, target, binding, snapshot, locations, ledger } = current;
73
+ if (canonical(value.pass) !== canonical(pass)) mutationFail14('ENTROROVE_MUTATION_PASS_CHANGED', 'Mutation pass changed while waiting for its original write lock');
74
+ return withSwarmMutationLease(input, pass, async () => {
75
+ const baseline = snapshot.manifest.files.find(file => file.path === target), content = contentFor(input, baseline);
76
+ const entry = { status: 'pending', contentDigest: sha256(content), at: new Date().toISOString() };
77
+ await saveMutationConsumption14(locations, { ...ledger, writes: { ...ledger.writes, [target]: entry } });
78
+ await auditMutation14(input, 'guarded-write-authorized', { passId: pass.passId, sha256: entry.contentDigest });
79
+ mutationLifetime14(pass);
80
+ const written = await writeMutationProjectFile14(input, binding.root, target, content, baseline);
81
+ await saveMutationConsumption14(locations, { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written' } } });
82
+ await auditMutation14(input, 'guarded-write', { passId: pass.passId, sha256: written.sha256 });
83
+ const stateRoot = await mutationStateRoot14(input);
84
+ await privateMutationDirectory14(path.join(stateRoot, 'ledgers', pass.repositoryDigest), { privateFrom: stateRoot });
85
+ await appendLedgerRecord(input, 'guarded-write', { flowId: pass.flowId, stepId: pass.stepId, targetPath: target,
86
+ sha256: written.sha256, passId: pass.passId });
87
+ return { schemaVersion: SCHEMA, status: 'verified', written: true, targetPath: target, sha256: written.sha256 };
88
+ });
89
+ }
90
+ export async function guardedWrite14(input) {
91
+ return withMutationAudit14(input, 'guarded-write', async () => {
92
+ const value = await checkedPass(input), pass = value.pass;
93
+ return withStateLock(value.locations.consumptionPath, () => withTaskHandoffWriteFence({ ...input,
94
+ epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => performWrite(input, state, value)));
95
+ });
96
+ }
@@ -0,0 +1,84 @@
1
+ import { createPublicKey, randomUUID, sign, verify } from 'node:crypto';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { loadTaskHandoffSigner } from './local-task-handoff-observation.mjs';
4
+ import { canonicalHandoffSignature } from './local-task-handoff-overlay-wire.mjs';
5
+ import { mutationFail14, mutationPath14 } from './local-mutation-files.mjs';
6
+ import { mutationLifetime14 } from './local-mutation-wire.mjs';
7
+
8
+ const identity = ['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision', 'stepId', 'leaseId',
9
+ 'generation', 'epoch', 'planId', 'planDigest', 'runnerId', 'runnerKeyId', 'repositoryId', 'repositoryDigest',
10
+ 'scopeBindingDigest', 'scopeArtifactDigest', 'scopeContractDigest', 'paths'];
11
+ function cloudContract(contract, pass) {
12
+ const rollback = contract?.schemaVersion === 'entrorove.merge-rollback-authorization/1.0';
13
+ try {
14
+ const encoded = process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY, der = Buffer.from(encoded ?? '', 'base64');
15
+ const key = createPublicKey({ key: der, type: 'spki', format: 'der' });
16
+ const { signature, ...payload } = contract;
17
+ if (key.asymmetricKeyType !== 'ed25519' || key.export({ type: 'spki', format: 'der' }).toString('base64') !== encoded
18
+ || (!rollback && contract.schemaVersion !== 'entrorove.merge-contract/1.0') || (rollback && contract.schemaVersion !== 'entrorove.merge-rollback-authorization/1.0')
19
+ || contract.keyId !== pass.authorityReceipt.keyId
20
+ || contract.keyId !== sha256(der) || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(contract.nonce)
21
+ || !verify(null, Buffer.from(canonical(payload)), key, canonicalHandoffSignature(signature))) throw new Error();
22
+ } catch { mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Batch merge contract requires its original pinned cloud signature'); }
23
+ mutationLifetime14(contract);
24
+ if (identity.some(field => canonical(contract[field] ?? null) !== canonical(pass[field] ?? null))
25
+ || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(contract.mergeId)) {
26
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Batch merge contract differs from the original signed pass identity');
27
+ }
28
+ }
29
+ function contractImages(contract, pass, before, after) {
30
+ const rollback = contract?.schemaVersion === 'entrorove.merge-rollback-authorization/1.0';
31
+ if (rollback) {
32
+ if (!Array.isArray(contract.beforeFiles) || !Array.isArray(contract.restoreFiles)) mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Rollback authorization has no frozen current and restore images');
33
+ const priorRollback = contract.beforeFiles.filter(file => pass.paths.includes(file?.path));
34
+ const nextRollback = contract.restoreFiles.filter(file => pass.paths.includes(file?.path));
35
+ if (canonical(priorRollback) !== canonical(before) || canonical(nextRollback) !== canonical(after)) mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Rollback images differ from the original cloud-frozen restore images');
36
+ return;
37
+ }
38
+ if (!Array.isArray(contract.beforeFullManifest) || !Array.isArray(contract.sourceFiles)) {
39
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Original merge contract has no frozen physical file images');
40
+ }
41
+ const prior = contract.beforeFullManifest.filter(file => pass.paths.includes(file?.path));
42
+ const next = contract.sourceFiles.filter(file => pass.paths.includes(file?.path)).map(file => ({
43
+ path: mutationPath14(file.path), exists: true, sha256: file.sha256, mode: file.mode }));
44
+ if (canonical(prior) !== canonical(before) || canonical(next) !== canonical(after)) {
45
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Batch images differ from the original cloud-frozen merge images');
46
+ }
47
+ }
48
+ export async function mutationBatchReceiptBinding14(input, original, options, before, after) {
49
+ const pass = original.pass, contractDigest = pass.executionPlanSha256;
50
+ let transactionId = pass.passId, contractExpiresAt;
51
+ if (options.contract !== undefined) {
52
+ cloudContract(options.contract, pass);
53
+ if (sha256(canonical(options.contract)) !== contractDigest) {
54
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Batch cannot replace the original pass-bound merge contract');
55
+ }
56
+ contractImages(options.contract, pass, before, after);
57
+ transactionId = options.contract.mergeId; contractExpiresAt = options.contract.expiresAt;
58
+ } else if (options.contractDigest !== undefined || options.transactionId !== undefined) {
59
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Caller receipt bindings require the original cloud-signed merge contract');
60
+ }
61
+ if (options.contractDigest !== undefined && options.contractDigest !== contractDigest
62
+ || options.transactionId !== undefined && options.transactionId !== transactionId) {
63
+ mutationFail14('ENTROROVE_MUTATION_BATCH_CONTRACT', 'Caller cannot redefine the original batch contract or transaction');
64
+ }
65
+ const signer = await loadTaskHandoffSigner(original.binding.context,
66
+ input.runnerSignerConfigPath ?? input.signerConfigPath, pass.runnerKeyId);
67
+ return { signer, contractDigest, transactionId, contractExpiresAt };
68
+ }
69
+ export function signMutationBatchReceipt14(state, status, actual, failure = {}) {
70
+ const now = Date.now(), pass = state.pass;
71
+ const expires = status === 'written' ? Math.min(now + state.signer.config.receiptTtlMs,
72
+ Date.parse(pass.expiresAt), state.contractExpiresAt ? Date.parse(state.contractExpiresAt) : Infinity)
73
+ : now + state.signer.config.receiptTtlMs;
74
+ if (expires <= now) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_EXPIRED', 'Original batch authority expired before its successful consumption proof');
75
+ const payload = { schemaVersion: 'entrorove.mutation-batch-consumption-receipt/1.0', status, consumed: true,
76
+ batchId: state.batchId, flowId: pass.flowId, taskId: pass.taskId, stepId: pass.stepId, leaseId: pass.leaseId,
77
+ generation: pass.generation, passDigest: state.passDigest, contractDigest: state.contractDigest,
78
+ transactionId: state.transactionId, executionAuthorityDigest: pass.executionAuthorityDigest,
79
+ executionAttemptNonce: pass.executionAttemptNonce, paths: [...pass.paths],
80
+ writes: actual.map(file => ({ path: file.path, status, contentDigest: file.sha256 })),
81
+ ...(status === 'written' ? { afterFiles: actual } : { currentFiles: actual, ...failure }),
82
+ keyId: pass.runnerKeyId, nonce: randomUUID(), issuedAt: new Date(now).toISOString(), expiresAt: new Date(expires).toISOString() };
83
+ return { ...payload, signature: sign(null, Buffer.from(canonical(payload)), state.signer.privateKey).toString('base64url') };
84
+ }