@entrorove/skill 0.1.61 → 0.1.64

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 (54) hide show
  1. package/CAPABILITY-MAP.md +7 -2
  2. package/CHANGELOG.md +15 -0
  3. package/IDE-EXECUTION.md +39 -6
  4. package/REUSE-DISCIPLINE.md +1 -1
  5. package/SKILL.md +6 -2
  6. package/cli-input.mjs +22 -0
  7. package/cli.mjs +2 -8
  8. package/cloud-task-client.mjs +10 -2
  9. package/cloud-task-images.mjs +39 -0
  10. package/ide-exit.mjs +49 -9
  11. package/ide-hook.mjs +2 -2
  12. package/local-contracts.mjs +4 -1
  13. package/local-discipline-gate.mjs +14 -3
  14. package/local-discipline-repository.mjs +11 -5
  15. package/local-finder-plist.mjs +87 -0
  16. package/local-finder-store.mjs +115 -0
  17. package/local-mutation-authority.mjs +11 -0
  18. package/local-mutation-completion-evidence.mjs +83 -0
  19. package/local-mutation-consumption.mjs +39 -14
  20. package/local-mutation-environment-observation.mjs +85 -0
  21. package/local-mutation-finder-observation.mjs +58 -0
  22. package/local-mutation-wire.mjs +28 -9
  23. package/local-preserved-mutation-protection.mjs +8 -1
  24. package/local-preserved-mutation-wire.mjs +7 -3
  25. package/local-scope-files.mjs +3 -1
  26. package/local-scope-font.mjs +45 -0
  27. package/local-scope-repository.mjs +16 -3
  28. package/local-scope-zip.mjs +93 -0
  29. package/manifest.json +1 -1
  30. package/member-producer-repair-authority.mjs +48 -0
  31. package/model-images.d.mts +9 -0
  32. package/model-images.mjs +87 -0
  33. package/mutation-challenge-refresh.mjs +51 -0
  34. package/mutation-client.mjs +45 -27
  35. package/mutation-connection-state.mjs +17 -0
  36. package/package.json +18 -2
  37. package/release-state.json +52 -36
  38. package/release.mjs +12 -4
  39. package/releases.json +18 -0
  40. package/remote-client.mjs +1 -1
  41. package/remote-transport.mjs +48 -16
  42. package/scope-amendment-client.mjs +1 -1
  43. package/scope-amendment-consumption.mjs +32 -0
  44. package/scope-amendment-mutation.mjs +3 -4
  45. package/scope-amendment-observer.mjs +7 -3
  46. package/scope-amendment-remote.mjs +12 -2
  47. package/scope-amendment-retirement.mjs +54 -0
  48. package/scope-amendment-state.mjs +7 -4
  49. package/scope-amendment-wire.mjs +3 -2
  50. package/task-entry-client.mjs +1 -0
  51. package/workflow-observation-client.mjs +11 -0
  52. package/workflow-observation-connection.mjs +2 -1
  53. package/workflow-observation-remote.mjs +26 -1
  54. package/workflow-observation-wire.mjs +6 -3
@@ -27,7 +27,7 @@ function sourceLimit(relative, bytes, sources, used, required) {
27
27
  `Semantic inspection of "${relative}" (${bytes} bytes) exceeds ${reason}; limits are ${disciplineAnalysisLimits.fileBytes} bytes per file, ${disciplineAnalysisLimits.sourceBytes} total bytes and ${disciplineAnalysisLimits.files} files. Preserve the original flow and split its approved inspection before retrying`);
28
28
  return reason;
29
29
  }
30
- async function integrityOnlyFile(root, relative) {
30
+ export async function repositoryIntegrityFile(root, relative) {
31
31
  // This path returns only an integrity digest. It never exposes source bytes or
32
32
  // contributes semantic facts outside the signed source-read scope.
33
33
  const before = await mutationTarget14(root, relative);
@@ -39,7 +39,9 @@ async function integrityOnlyFile(root, relative) {
39
39
  for await (const chunk of handle.createReadStream({ autoClose: false })) digest.update(chunk);
40
40
  const last = await handle.stat(), current = await mutationTarget14(root, relative);
41
41
  if (!sameFile(first, last) || !sameFile(last, current.stat)) mutationFail14('ENTROROVE_FILE_CHANGED', `${relative} changed during integrity observation`);
42
- return { sha256: digest.digest('hex'), mode: last.mode & 0o777 };
42
+ return { sha256: digest.digest('hex'), mode: last.mode & 0o777,
43
+ identity: { dev: last.dev, ino: last.ino, nlink: last.nlink, birthtimeMs: last.birthtimeMs,
44
+ ctimeMs: last.ctimeMs, mtimeMs: last.mtimeMs, size: last.size } };
43
45
  } finally { await handle.close(); }
44
46
  }
45
47
  async function inventory(root, prefix = '') {
@@ -77,7 +79,7 @@ export async function disciplineRepositoryObservation(input, root, { analyze = f
77
79
  const allowed = !restricted || withinBudgetScope(relative, restricted);
78
80
  const bytes = overrides.has(relative) ? Buffer.byteLength(overrides.get(relative)) : stat.size;
79
81
  const limited = semantic && allowed ? sourceLimit(relative, bytes, sources, sourceBytes, required) : null;
80
- const read = semantic && allowed && !limited ? await mutationRead14(input, root, relative) : await integrityOnlyFile(root, relative);
82
+ const read = semantic && allowed && !limited ? await mutationRead14(input, root, relative) : await repositoryIntegrityFile(root, relative);
81
83
  files.push({ path: relative, sha256: read.sha256, mode: read.mode, kind: 'file' });
82
84
  if (semantic && allowed && !limited) { sources.push({ path: relative, content: overrides.get(relative) ?? read.content.toString('utf8') }); sourceBytes += bytes; }
83
85
  else if (limited) unresolved.push({ path: relative, reason: limited, bytes });
@@ -124,7 +126,7 @@ export function disciplineRepositoryDifferences(before, after, authorized = new
124
126
  }
125
127
  return changed;
126
128
  }
127
- export async function assertDisciplineRepository(input, root, review, ledger) {
129
+ export async function observeDisciplineRepositoryState(input, root, review, ledger) {
128
130
  const expected = new Map();
129
131
  for (const [relative, write] of Object.entries(ledger.writes ?? {})) {
130
132
  if (write.status !== 'written') mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Repository comparison found an unresolved physical action');
@@ -137,6 +139,10 @@ export async function assertDisciplineRepository(input, root, review, ledger) {
137
139
  }
138
140
  const current = await disciplineRepositoryObservation(input, root);
139
141
  const changed = disciplineRepositoryDifferences(review.comparison, current.comparison, expected);
142
+ return { comparison: current.comparison, changed };
143
+ }
144
+ export async function assertDisciplineRepository(input, root, review, ledger) {
145
+ const { comparison, changed } = await observeDisciplineRepositoryState(input, root, review, ledger);
140
146
  if (changed.length) mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT', `Whole-repository comparison differs outside completed declared actions: ${changed.join(', ')}`);
141
- return current.comparison;
147
+ return comparison;
142
148
  }
@@ -0,0 +1,87 @@
1
+ // Bounded structural inspection only. No property values leave this reader.
2
+ const invalid = () => { throw new Error('invalid-finder-property-list'); };
3
+ function integer(bytes, offset, size, end = bytes.length) {
4
+ if (![1, 2, 4, 8].includes(size) || offset < 0 || offset + size > end) invalid();
5
+ let value = 0n;
6
+ for (let index = offset; index < offset + size; index++) value = value * 256n + BigInt(bytes[index]);
7
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) invalid();
8
+ return Number(value);
9
+ }
10
+ function length(bytes, start, end, info) {
11
+ if (info !== 15) return { count: info, offset: start };
12
+ const marker = bytes[start];
13
+ if (marker === undefined || marker >> 4 !== 1 || (marker & 15) > 3) invalid();
14
+ const size = 2 ** (marker & 15);
15
+ return { count: integer(bytes, start + 1, size, end), offset: start + 1 + size };
16
+ }
17
+ function string(bytes, start, count, width, end) {
18
+ const size = count * width;
19
+ if (count > 65536 || start + size > end) invalid();
20
+ const content = bytes.subarray(start, start + size);
21
+ if (width === 1 && content.some(value => value > 127)) invalid();
22
+ return new TextDecoder(width === 2 ? 'utf-16be' : 'ascii', { fatal: true }).decode(content);
23
+ }
24
+ function object(state, id, depth) {
25
+ if (!Number.isSafeInteger(id) || id < 0 || id >= state.offsets.length || depth > 64 || state.visiting.has(id)) invalid();
26
+ if (state.read.has(id)) return state.read.get(id);
27
+ state.visiting.add(id);
28
+ const start = state.offsets[id], end = state.ends.get(start), marker = state.bytes[start];
29
+ if (marker === undefined) invalid();
30
+ const type = marker >> 4, info = marker & 15;
31
+ let cursor = start + 1, text = null;
32
+ if (type === 0) { if (![0, 8, 9].includes(info)) invalid(); }
33
+ else if (type === 1) {
34
+ if (info > 4) invalid();
35
+ cursor += 2 ** info;
36
+ } else if (type === 2 || type === 3) {
37
+ const size = type === 3 ? 8 : 2 ** info;
38
+ if (type === 3 && info !== 3 || ![4, 8].includes(size) || cursor + size > end) invalid();
39
+ const value = size === 4 ? state.bytes.readFloatBE(cursor) : state.bytes.readDoubleBE(cursor);
40
+ if (!Number.isFinite(value)) invalid();
41
+ cursor += size;
42
+ } else if ([4, 5, 6, 10, 13].includes(type)) {
43
+ const value = length(state.bytes, cursor, end, info); cursor = value.offset;
44
+ if (value.count > 1_048_576) invalid();
45
+ if (type === 4) cursor += value.count;
46
+ else if (type === 5 || type === 6) {
47
+ text = string(state.bytes, cursor, value.count, type === 6 ? 2 : 1, end);
48
+ cursor += value.count * (type === 6 ? 2 : 1);
49
+ } else {
50
+ if (value.count > 10000) invalid();
51
+ const total = value.count * (type === 13 ? 2 : 1), keys = new Set();
52
+ for (let index = 0; index < total; index++) {
53
+ const reference = integer(state.bytes, cursor, state.referenceSize, end); cursor += state.referenceSize;
54
+ const child = object(state, reference, depth + 1);
55
+ if (type === 13 && index < value.count) {
56
+ if (child.text === null || keys.has(child.text)) invalid();
57
+ keys.add(child.text);
58
+ }
59
+ }
60
+ }
61
+ } else if (type === 8) cursor += info + 1;
62
+ else invalid();
63
+ if (cursor > end) invalid();
64
+ state.visiting.delete(id);
65
+ const result = { text }; state.read.set(id, result); return result;
66
+ }
67
+ export function validateFinderPropertyList(bytes) {
68
+ if (!Buffer.isBuffer(bytes) || bytes.length < 40 || bytes.length > 1_048_576
69
+ || bytes.subarray(0, 8).toString('latin1') !== 'bplist00') invalid();
70
+ const trailer = bytes.length - 32;
71
+ if (bytes.subarray(trailer, trailer + 6).some(value => value !== 0)) invalid();
72
+ const offsetSize = bytes[trailer + 6], referenceSize = bytes[trailer + 7];
73
+ if (![1, 2, 4, 8].includes(offsetSize) || ![1, 2, 4, 8].includes(referenceSize)) invalid();
74
+ const count = integer(bytes, trailer + 8, 8), top = integer(bytes, trailer + 16, 8), table = integer(bytes, trailer + 24, 8);
75
+ if (!count || count > 10000 || top >= count || table < 8 || table + count * offsetSize !== trailer) invalid();
76
+ const offsets = Array.from({ length: count }, (_, index) => integer(bytes, table + index * offsetSize, offsetSize));
77
+ if (new Set(offsets).size !== count || offsets.some(offset => offset < 8 || offset >= table)) invalid();
78
+ const sorted = [...offsets].sort((a, b) => a - b), ends = new Map(sorted.map((offset, index) => [offset, sorted[index + 1] ?? table]));
79
+ const state = { bytes, offsets, ends, referenceSize, read: new Map(), visiting: new Set() };
80
+ if (bytes[offsets[top]] >> 4 !== 13) invalid();
81
+ object(state, top, 0);
82
+ // Finder can retain unreferenced duplicate scalar objects. Inspect those
83
+ // too; an unused object is not an exemption from structural validation.
84
+ for (let id = 0; id < count; id++) object(state, id, 0);
85
+ if (state.read.size !== count) invalid();
86
+ return { objects: count };
87
+ }
@@ -0,0 +1,115 @@
1
+ import { validateFinderPropertyList } from './local-finder-plist.mjs';
2
+
3
+ const invalid = () => { throw new Error('invalid-finder-store'); };
4
+ function reader(bytes, start = 0, end = bytes.length) {
5
+ let cursor = start;
6
+ return {
7
+ take(size) {
8
+ if (!Number.isSafeInteger(size) || size < 0 || cursor + size > end) invalid();
9
+ const result = bytes.subarray(cursor, cursor + size); cursor += size; return result;
10
+ },
11
+ number() { return this.take(4).readUInt32BE(); },
12
+ byte() { return this.take(1)[0]; },
13
+ };
14
+ }
15
+ function allocation(bytes) {
16
+ if (bytes.length < 2048 || bytes.length > 1_048_576 || bytes.subarray(0, 8).toString('hex') !== '0000000142756431') invalid();
17
+ const header = reader(bytes, 8, 36), offset = header.number(), size = header.number();
18
+ if (header.number() !== offset || offset < 32 || size < 1024 || offset + size + 4 > bytes.length) invalid();
19
+ const book = reader(bytes, offset + 4, offset + size + 4), count = book.number();
20
+ if (count < 3 || count > 8192 || book.number() !== 0) invalid();
21
+ const blocks = [], intervals = [[0, 32]];
22
+ for (let index = 0; index < count; index++) {
23
+ const address = book.number();
24
+ if (!address) { blocks.push(null); continue; }
25
+ const power = address & 31, start = (address & 0xffffffe0) >>> 0, length = 2 ** power;
26
+ if (power < 5 || power > 20 || start < 32 || start % length || start + length + 4 > bytes.length) invalid();
27
+ blocks.push({ offset: start, length }); intervals.push([start, start + length]);
28
+ }
29
+ if (book.take((Math.ceil(count / 256) * 256 - count) * 4).some(byte => byte !== 0)) invalid();
30
+ if (blocks[0]?.offset !== offset || blocks[0].length < size || book.number() !== 1 || book.byte() !== 4
31
+ || book.take(4).toString('latin1') !== 'DSDB') invalid();
32
+ const master = book.number();
33
+ if (!master || !blocks[master]) invalid();
34
+ for (let power = 0; power < 32; power++) {
35
+ const entries = book.number();
36
+ if (entries > 8192 || power < 5 && entries) invalid();
37
+ for (let index = 0; index < entries; index++) {
38
+ const start = book.number(), length = 2 ** power;
39
+ if (start < 32 || start % length || start + length > 2 ** 31) invalid();
40
+ intervals.push([start, start + length]);
41
+ }
42
+ }
43
+ intervals.sort((a, b) => a[0] - b[0]);
44
+ if (intervals.some((range, index) => index && range[0] !== intervals[index - 1][1])
45
+ || intervals.at(-1)[1] !== 2 ** 31 || Math.max(...blocks.filter(Boolean).map(block => block.offset + block.length)) + 4 !== bytes.length) invalid();
46
+ return { blocks, master };
47
+ }
48
+ const fixedBlobs = new Map([['Iloc', [16]], ['dilc', [32]], ['fwi0', [16]], ['icgo', [8]], ['icsp', [8]],
49
+ ['icvo', [18, 26]], ['info', [40, 48]], ['lssp', [8]], ['lsvo', [76]], ['BKGD', [12]]]);
50
+ const plistBlobs = new Set(['bwsp', 'icvp', 'lsvp', 'lsvP']);
51
+ const expectedTypes = new Map([
52
+ ...[...fixedBlobs.keys(), ...plistBlobs].map(code => [code, 'blob']),
53
+ ...['ICVO', 'LSVO', 'dscl'].map(code => [code, 'bool']),
54
+ ...['fwsw', 'vSrn'].map(code => [code, 'long']),
55
+ ...['fwvh', 'icvt', 'lsvt'].map(code => [code, 'shor']),
56
+ ...['cmmt', 'extn', 'GRP0'].map(code => [code, 'ustr']),
57
+ ...['logS', 'lg1S', 'phyS', 'ph1S'].map(code => [code, 'comp']),
58
+ ...['modD', 'moDD'].map(code => [code, 'dutc']), ['vstl', 'type'],
59
+ ]);
60
+ function unicode(input) {
61
+ const count = input.number();
62
+ if (count > 65536) invalid();
63
+ const value = new TextDecoder('utf-16be', { fatal: true }).decode(input.take(count * 2));
64
+ if (value.includes('\0')) invalid();
65
+ return value;
66
+ }
67
+ function record(input) {
68
+ const name = unicode(input), code = input.take(4).toString('latin1'), type = input.take(4).toString('latin1');
69
+ if (!name || name.length > 1024 || name.includes('/') || expectedTypes.get(code) !== type) invalid();
70
+ if (type === 'blob') {
71
+ const blob = input.take(input.number());
72
+ if (plistBlobs.has(code)) validateFinderPropertyList(blob);
73
+ else if (!fixedBlobs.get(code).includes(blob.length)) invalid();
74
+ if (code === 'BKGD' && !['DefB', 'ClrB'].includes(blob.subarray(0, 4).toString('latin1'))) invalid();
75
+ } else if (type === 'ustr') unicode(input);
76
+ else if (type === 'bool') { if (input.byte() > 1) invalid(); }
77
+ else if (type === 'comp' || type === 'dutc') input.take(8);
78
+ else if (type === 'shor') { if (input.number() > 65535) invalid(); }
79
+ else if (type === 'long') input.number();
80
+ else if (!['icnv', 'clmv', 'Nlsv', 'Flwv'].includes(input.take(4).toString('latin1'))) invalid();
81
+ return name.normalize('NFD').toLowerCase() + '\0' + code;
82
+ }
83
+ function tree(bytes, blocks, master) {
84
+ const readBlock = id => {
85
+ const block = blocks[id];
86
+ if (!block) invalid();
87
+ return reader(bytes, block.offset + 4, block.offset + block.length + 4);
88
+ };
89
+ const info = readBlock(master), root = info.number(), levels = info.number(), records = info.number(), nodes = info.number(), pageSize = info.number();
90
+ if (levels > 32 || records > 10000 || !nodes || nodes > 8190 || pageSize !== 4096) invalid();
91
+ const visited = new Set([0, master]);
92
+ let actualRecords = 0, previous = null;
93
+ const walk = (id, level) => {
94
+ if (visited.has(id) || !blocks[id] || blocks[id].length > pageSize) invalid();
95
+ visited.add(id);
96
+ const input = readBlock(id), right = input.number(), count = input.number();
97
+ if (Boolean(right) !== Boolean(level) || count > 10000 || actualRecords + count > records) invalid();
98
+ for (let index = 0; index < count; index++) {
99
+ if (right) walk(input.number(), level - 1);
100
+ const key = record(input);
101
+ if (previous !== null && previous >= key) invalid();
102
+ previous = key; actualRecords++;
103
+ }
104
+ if (right) walk(right, level - 1);
105
+ };
106
+ walk(root, levels);
107
+ if (actualRecords !== records || visited.size !== nodes + 2
108
+ || blocks.some((block, id) => block && !visited.has(id))) invalid();
109
+ return { format: 'finder-ds-store/1.0', records, nodes, bytes: bytes.length };
110
+ }
111
+ export function validateFinderStore(bytes) {
112
+ if (!Buffer.isBuffer(bytes)) invalid();
113
+ const { blocks, master } = allocation(bytes);
114
+ return tree(bytes, blocks, master);
115
+ }
@@ -92,6 +92,16 @@ function contentFor(input, baseline) {
92
92
  if (input.expectedSha256 !== undefined && input.expectedSha256 !== null && input.expectedSha256 !== baseline.sha256) {
93
93
  mutationFail14('ENTROROVE_WRITE_BASELINE', 'Target differs from the expected snapshot hash');
94
94
  }
95
+ if (Object.hasOwn(input, 'contentBase64')) {
96
+ if (Object.hasOwn(input, 'content') || typeof input.contentBase64 !== 'string'
97
+ || input.contentBase64.length > 16_000_000 || input.contentBase64.length % 4
98
+ || !/^[A-Za-z0-9+/]*={0,2}$/.test(input.contentBase64)) {
99
+ mutationFail14('ENTROROVE_CONTENT_INVALID', 'Use exactly one text/JSON content or canonical bounded contentBase64');
100
+ }
101
+ const bytes = Buffer.from(input.contentBase64, 'base64');
102
+ if (bytes.toString('base64') !== input.contentBase64) mutationFail14('ENTROROVE_CONTENT_INVALID', 'Binary content must use canonical base64');
103
+ return bytes;
104
+ }
95
105
  let content; try { content = typeof input.content === 'string' ? input.content : JSON.stringify(input.content); }
96
106
  catch { mutationFail14('ENTROROVE_CONTENT_INVALID', 'Guarded-write content must be finite JSON or text'); }
97
107
  if (typeof content !== 'string') mutationFail14('ENTROROVE_CONTENT_INVALID', 'Guarded-write content must be JSON or text');
@@ -103,6 +113,7 @@ async function performWrite(input, state, value) {
103
113
  return withSwarmMutationLease(input, pass, async () => {
104
114
  const baseline = snapshot.manifest.files.find(file => file.path === target), content = contentFor(input, baseline);
105
115
  const discipline = await disciplineWritePreflight(input, current, content);
116
+ mutationLifetime14(pass);
106
117
  const entry = { status: 'pending', contentDigest: sha256(content), mode: baseline.mode ?? 0o644, at: new Date().toISOString() };
107
118
  await saveMutationConsumption14(locations, { ...ledger, writes: { ...ledger.writes, [target]: entry } });
108
119
  await auditMutation14(input, 'guarded-write-authorized', { passId: pass.passId, sha256: entry.contentDigest,
@@ -0,0 +1,83 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createInterface } from 'node:readline';
4
+ import { canonical, sha256 } from './local-integrity.mjs';
5
+ import { mutationFile14, mutationStateRoot14, readMutationJson14, mutationFail14 } from './local-mutation-files.mjs';
6
+
7
+ const fail = () => mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT',
8
+ 'Post-write recovery requires the original successful write audit and intact execution ledger; no baseline or permission was changed');
9
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
10
+
11
+ function checkedLedger(value, repositoryDigest) {
12
+ if (value.schemaVersion !== 'entrorove.execution-ledger/1.0' || value.repositoryDigest !== repositoryDigest
13
+ || !Array.isArray(value.records) || value.records.length > 100_000 || value.sequence !== value.records.length) fail();
14
+ let previous = null;
15
+ for (const [index, record] of value.records.entries()) {
16
+ const { digest, previousDigest, ...unsigned } = record;
17
+ if (record.sequence !== index + 1 || previousDigest !== previous || !iso(record.at)
18
+ || digest !== sha256(canonical(unsigned) + (previous ?? ''))) fail();
19
+ previous = digest;
20
+ }
21
+ if (value.head !== previous) fail();
22
+ return value.records;
23
+ }
24
+
25
+ async function matchingAudit(file, pass, candidates) {
26
+ const item = await mutationFile14(file);
27
+ if (item.stat.nlink !== 1 || item.stat.size > 256 * 1024 * 1024
28
+ || process.platform !== 'win32' && (item.stat.mode & 0o077)
29
+ || process.getuid && item.stat.uid !== process.getuid()) fail();
30
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
31
+ const matched = new Map();
32
+ try {
33
+ const before = await handle.stat();
34
+ if (before.dev !== item.stat.dev || before.ino !== item.stat.ino || before.size < item.stat.size) fail();
35
+ if (item.stat.size) {
36
+ const lines = createInterface({ input: handle.createReadStream({ start: 0, end: item.stat.size - 1, autoClose: false }), crlfDelay: Infinity });
37
+ try {
38
+ for await (const line of lines) {
39
+ if (Buffer.byteLength(line) > 1_048_576) fail();
40
+ let record; try { record = JSON.parse(line); } catch { fail(); }
41
+ if (record.event !== 'guarded-write' || record.passId !== pass.passId) continue;
42
+ if (record.schemaVersion !== 'entrorove.local-execution/1.0' || record.flowId !== pass.flowId
43
+ || record.stepId !== pass.stepId || record.repositoryDigest !== pass.repositoryDigest || !iso(record.at)) fail();
44
+ const candidate = candidates.get(record.targetPathDigest);
45
+ if (!candidate || matched.has(candidate.targetPath) || record.sha256 !== candidate.sha256
46
+ || Date.parse(record.at) < Date.parse(candidate.writtenAt) || Date.parse(record.at) > Date.parse(candidate.at)) fail();
47
+ matched.set(candidate.targetPath, record);
48
+ }
49
+ } finally { lines.close(); }
50
+ }
51
+ const after = await handle.stat(), current = await mutationFile14(file);
52
+ if (after.dev !== item.stat.dev || after.ino !== item.stat.ino || after.nlink !== 1
53
+ || after.size < item.stat.size || current.stat.dev !== after.dev || current.stat.ino !== after.ino) fail();
54
+ return matched;
55
+ } finally { await handle.close(); }
56
+ }
57
+
58
+ export async function mutationCompletionEvidence(input, state) {
59
+ const { pass, ledger } = state, stateRoot = await mutationStateRoot14(input);
60
+ let records;
61
+ try { records = checkedLedger(await readMutationJson14(path.join(stateRoot, 'ledgers', pass.repositoryDigest, 'state.json')), pass.repositoryDigest); }
62
+ catch { fail(); }
63
+ const candidates = new Map();
64
+ for (const [targetPath, written] of Object.entries(ledger.writes)) {
65
+ if (written.status !== 'written') continue;
66
+ const matches = records.filter(record => record.event === 'guarded-write' && record.passId === pass.passId && record.targetPath === targetPath);
67
+ const record = matches[0];
68
+ if (matches.length !== 1 || record.flowId !== pass.flowId || record.stepId !== pass.stepId
69
+ || record.sha256 !== written.contentDigest || !iso(written.writtenAt)
70
+ || Date.parse(record.at) < Date.parse(written.writtenAt) || Date.parse(record.at) > Date.now()) fail();
71
+ candidates.set(sha256(targetPath), { ...record, writtenAt: written.writtenAt });
72
+ }
73
+ if (!candidates.size) fail();
74
+ let audits;
75
+ try { audits = await matchingAudit(path.join(stateRoot, 'invocations.jsonl'), pass, candidates); } catch { fail(); }
76
+ return [...candidates.values()].sort((a, b) => a.targetPath < b.targetPath ? -1 : 1).map(record => {
77
+ const audit = audits.get(record.targetPath);
78
+ if (!audit) fail();
79
+ return { path: record.targetPath, sha256: record.sha256, writtenAt: record.writtenAt, completedAt: record.at,
80
+ ledgerSequence: record.sequence, ledgerRecordDigest: record.digest, auditRecordDigest: sha256(canonical(audit)),
81
+ source: 'original-successful-write-audit-and-ledger' };
82
+ });
83
+ }
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { randomUUID, sign, verify } from 'node:crypto';
3
3
  import { canonical, sha256 } from './local-integrity.mjs';
4
4
  import { withStateLock } from './local-state-lock.mjs';
5
- import { mutationFail14, mutationRead14, readMutationJson14, atomicMutationJson14, privateMutationDirectory14 } from './local-mutation-files.mjs';
5
+ import { mutationFail14, mutationRead14, mutationTarget14, readMutationJson14, atomicMutationJson14, privateMutationDirectory14 } from './local-mutation-files.mjs';
6
6
  import { readStoredMutationPass14, readMutationConsumption14, mutationPassLocations14 } from './local-mutation-pass-store.mjs';
7
7
  import { assertPreservedMutationIssued } from './local-preserved-mutation-state.mjs';
8
8
  import { loadPreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
@@ -10,10 +10,11 @@ import { preservedMutationPublicKey } from './local-preserved-mutation-wire.mjs'
10
10
  import { assertPreservedMutationProtection } from './local-preserved-mutation-protection.mjs';
11
11
  import { snapshotAuthority14, readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
12
12
  import { assertDelegatedSourceModification } from './local-delegated-modify-analysis.mjs';
13
- import { assertDisciplineRepository } from './local-discipline-repository.mjs';
13
+ import { observeCompletedMutationEnvironment, readMutationEnvironmentObservation } from './local-mutation-environment-observation.mjs';
14
14
  import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
15
+ import { assertDisciplineRepository } from './local-discipline-repository.mjs';
15
16
 
16
- const fail = () => mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_INVALID', 'Mutation consumption requires its actual completed original writes, unchanged protection and separate M signer');
17
+ const fail = () => mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_INVALID', 'Mutation consumption requires actual original writes or expired unchanged unwritten targets, intact protection and the separate M signer');
17
18
  const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
18
19
  export async function preservedMutationConsumptionState(input) {
19
20
  const pass = await readStoredMutationPass14(input, { live: false });
@@ -24,36 +25,58 @@ export async function preservedMutationConsumptionState(input) {
24
25
  }
25
26
  async function actualWrites(input, state) {
26
27
  const { pass, binding, ledger } = state;
27
- if (!state.complete || Object.keys(ledger.writes).length !== pass.paths.length) fail();
28
+ if (!state.complete && Date.now() < Date.parse(pass.expiresAt)) fail();
28
29
  const snapshot = await snapshotAuthority14(input, binding.root, pass.snapshotReceipt);
29
30
  if (sha256(canonical(snapshot.manifest)) !== pass.snapshotManifestDigest || snapshot.manifest.snapshotDigest !== pass.snapshotDigest) fail();
30
- const writes = [];
31
+ const writes = [], unwritten = [];
32
+ const create = pass.writeDelegation.purpose === 'planned-file-create';
31
33
  for (const target of pass.paths) {
32
34
  const completed = ledger.writes[target], before = snapshot.manifest.files.find(file => file.path === target);
35
+ if (completed?.status !== 'written') {
36
+ if (!before || completed && (completed.status !== 'pending' || !iso(completed.at)
37
+ || Date.parse(completed.at) < Date.parse(pass.expiresAt) || Date.parse(completed.at) > Date.now())) fail();
38
+ const item = await mutationTarget14(binding.root, target, true);
39
+ const current = item.exists ? await mutationRead14(input, binding.root, target)
40
+ : { sha256: sha256('entrorove.missing-file/1.0'), mode: null };
41
+ if (item.exists !== before.exists || current.sha256 !== before.sha256 || current.mode !== before.mode) fail();
42
+ unwritten.push({ path: target, beforeSha256: before.sha256, beforeMode: before.mode,
43
+ observedSha256: current.sha256, observedMode: current.mode, ledgerStatus: completed ? 'expired-before-write' : 'untouched',
44
+ attemptedAt: completed?.at ?? null, attemptedContentDigest: completed?.contentDigest ?? null });
45
+ continue;
46
+ }
33
47
  if (!iso(completed.writtenAt)) mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_WRITE_TIME', 'Original write has no verifiable completion timestamp; its old receipt cannot authorize another mutation');
34
- if (!before?.exists || completed.kind && completed.kind !== 'modify' || !iso(completed.at)
48
+ if (!before || (create ? before.exists !== false || before.mode !== null : !before.exists)
49
+ || completed.kind && completed.kind !== 'modify' || !iso(completed.at)
35
50
  || Date.parse(completed.writtenAt) < Date.parse(pass.issuedAt) || Date.parse(completed.writtenAt) >= Date.parse(pass.expiresAt)
36
51
  || Date.parse(completed.writtenAt) < Date.parse(completed.at) || Date.parse(completed.writtenAt) > Date.now()) fail();
37
52
  const current = await mutationRead14(input, binding.root, target), copy = await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target);
38
- if (current.sha256 !== completed.contentDigest || current.mode !== completed.mode || current.mode !== before.mode) fail();
39
- assertDelegatedSourceModification({ path: target, before: copy.content.toString('utf8'), after: current.content.toString('utf8') });
53
+ if (current.sha256 !== completed.contentDigest || current.mode !== completed.mode || current.mode !== (create ? 0o644 : before.mode)) fail();
54
+ if (pass.writeDelegation.purpose === 'preserved-existing-modify') assertDelegatedSourceModification({ path: target, before: copy.content.toString('utf8'), after: current.content.toString('utf8') });
40
55
  writes.push({ path: target, beforeSha256: before.sha256, beforeMode: before.mode,
41
56
  afterSha256: current.sha256, afterMode: current.mode, writtenAt: completed.writtenAt });
42
57
  }
43
- await assertDisciplineRepository(input, binding.root, await readDisciplineSnapshot(snapshot), ledger);
44
58
  await assertPreservedMutationProtection(input, binding.root, pass);
45
- return writes;
59
+ const review = await readDisciplineSnapshot(snapshot);
60
+ const writtenLedger = { ...ledger, writes: Object.fromEntries(Object.entries(ledger.writes).filter(([, value]) => value.status === 'written')) };
61
+ if (!writes.length) await assertDisciplineRepository(input, binding.root, review, writtenLedger);
62
+ else await observeCompletedMutationEnvironment(input, state, review, writtenLedger);
63
+ return { writes, ...(unwritten.length ? { unwritten, reconciledAt: new Date(Date.now()).toISOString() } : {}) };
46
64
  }
47
- function receiptCore(pass, writes) {
48
- return { schemaVersion: 'entrorove.preserved-mutation-consumption/1.0', purpose: 'preserved-existing-modify-consumption',
65
+ function receiptCore(pass, physical) {
66
+ return { schemaVersion: 'entrorove.preserved-mutation-consumption/1.0', purpose: `${pass.writeDelegation.purpose}-consumption`,
49
67
  keyId: pass.writeDelegation.mutationKeyId, ownerId: pass.ownerId, taskId: pass.taskId, flowId: pass.flowId, stepId: pass.stepId,
50
68
  leaseId: pass.leaseId, generation: pass.generation, epoch: pass.epoch, passId: pass.passId, passDigest: sha256(canonical(pass)),
51
69
  challengeId: pass.challengeId, challengeNonce: pass.challengeNonce, passNonce: pass.nonce,
52
70
  snapshotId: pass.snapshotId, snapshotDigest: pass.snapshotDigest,
53
- protectedFilesDigest: sha256(canonical(pass.writeDelegation.protectedFiles)), writes };
71
+ protectedFilesDigest: sha256(canonical(pass.writeDelegation.protectedFiles)), ...physical };
54
72
  }
55
73
  function cachedReceipt(value, pass, core) {
56
74
  const { signature, issuedAt, expiresAt, nonce, ...actual } = value;
75
+ if (core.reconciledAt) {
76
+ if (!iso(actual.reconciledAt) || Date.parse(actual.reconciledAt) < Date.parse(pass.expiresAt)
77
+ || Date.parse(actual.reconciledAt) > Date.parse(issuedAt)) fail();
78
+ core = { ...core, reconciledAt: actual.reconciledAt };
79
+ }
57
80
  const unsigned = { ...actual, issuedAt, expiresAt, nonce }, bytes = typeof signature === 'string' ? Buffer.from(signature, 'base64url') : Buffer.alloc(0);
58
81
  if (canonical(actual) !== canonical(core) || !iso(issuedAt) || !iso(expiresAt) || Date.parse(issuedAt) > Date.now()
59
82
  || Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) - Date.parse(issuedAt) > 300_000
@@ -62,7 +85,7 @@ function cachedReceipt(value, pass, core) {
62
85
  return value;
63
86
  }
64
87
  async function consumptionReceipt(input, state, { renewExpired = false, allowExpiredReplay = false } = {}) {
65
- const writes = await actualWrites(input, state), core = receiptCore(state.pass, writes);
88
+ const physical = await actualWrites(input, state), core = receiptCore(state.pass, physical);
66
89
  const directory = path.join(state.binding.directory, 'consumption-receipts'), file = path.join(directory, `${state.pass.passId}.json`);
67
90
  await privateMutationDirectory14(directory, { privateFrom: state.binding.stateRoot });
68
91
  let previous;
@@ -86,7 +109,9 @@ export async function attestMutationConsumption14(input, options = {}) {
86
109
  const original = await preservedMutationConsumptionState(input);
87
110
  return withStateLock(original.locations.consumptionPath, async () => {
88
111
  const state = await preservedMutationConsumptionState(input), receipt = await consumptionReceipt(input, state, options);
112
+ const environmentObservation = await readMutationEnvironmentObservation(input, state.pass);
89
113
  return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', consumptionReceipt: receipt,
114
+ ...(environmentObservation ? { environmentObservation, environmentObservationAcknowledgedByServer: false } : {}),
90
115
  claimsVerifiedExecution: false, claimsCompletion: false,
91
116
  nextAction: { operation: 'mutation-pass-settle', input: { passId: state.pass.passId, consumptionReceipt: receipt } } };
92
117
  });
@@ -0,0 +1,85 @@
1
+ import path from 'node:path';
2
+ import { randomUUID, sign, verify } from 'node:crypto';
3
+ import { canonical, sha256 } from './local-integrity.mjs';
4
+ import { mutationFail14, mutationStateRoot14, atomicMutationJson14, readMutationJson14 } from './local-mutation-files.mjs';
5
+ import { observeDisciplineRepositoryState, repositoryIntegrityFile } from './local-discipline-repository.mjs';
6
+ import { mutationCompletionEvidence } from './local-mutation-completion-evidence.mjs';
7
+ import { loadPreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
8
+ import { preservedMutationPublicKey } from './local-preserved-mutation-wire.mjs';
9
+ import { finderMetadataPath, observeFinderMetadataChanges } from './local-mutation-finder-observation.mjs';
10
+
11
+ const schemaVersion = 'entrorove.post-write-environment-observation/1.0';
12
+ const fail = () => mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT',
13
+ 'Environment drift is not a proven new regular file created after the expired pass and its original successful write checks; original source protection remains enforced');
14
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
15
+ const attribution = changes => changes.some(change => change?.kind === 'validated-finder-metadata')
16
+ ? changes.every(change => change?.kind === 'validated-finder-metadata')
17
+ ? 'unattributed-post-write-finder-metadata' : 'unattributed-post-write-environment'
18
+ : 'unattributed-post-write-additions';
19
+ function location(stateRoot, pass) {
20
+ return path.join(stateRoot, 'post-write-environment', pass.repositoryDigest, pass.passId + '.json');
21
+ }
22
+ function verified(value, pass) {
23
+ const { signature, ...unsigned } = value ?? {}, key = preservedMutationPublicKey(pass.writeDelegation.mutationPublicKey);
24
+ const expected = { schemaVersion, keyId: pass.writeDelegation.mutationKeyId, ownerId: pass.ownerId, taskId: pass.taskId,
25
+ flowId: pass.flowId, stepId: pass.stepId, passId: pass.passId, passDigest: sha256(canonical(pass)),
26
+ originalReviewDigest: pass.snapshotReceipt.disciplineReviewDigest,
27
+ attribution: attribution(Array.isArray(value?.changes) ? value.changes : []), grantsWriteAuthority: false,
28
+ claimsRepositoryUnchanged: false, claimsAuthorizedEnvironmentChange: false };
29
+ if (Object.entries(expected).some(([field, expected]) => canonical(value?.[field]) !== canonical(expected))
30
+ || !iso(value.observedAt) || Date.parse(value.observedAt) > Date.now()
31
+ || !Array.isArray(value.changes) || !value.changes.length || !Array.isArray(value.completionEvidence)
32
+ || !verify(null, Buffer.from(canonical(unsigned)), key.key, Buffer.from(signature ?? '', 'base64url'))) fail();
33
+ return value;
34
+ }
35
+ export async function readMutationEnvironmentObservation(input, pass) {
36
+ try { return verified(await readMutationJson14(location(await mutationStateRoot14(input), pass)), pass); }
37
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
38
+ }
39
+ async function additions(root, review, comparison, changed, state, completionEvidence) {
40
+ const { pass } = state, before = new Set(review.comparison.files.map(file => file.path));
41
+ const protectedPaths = new Set([...pass.paths, ...pass.writeDelegation.protectedFiles.map(file => file.path)]);
42
+ const completedAt = Math.max(Date.parse(pass.expiresAt), ...completionEvidence.map(record => Date.parse(record.completedAt)));
43
+ if (Date.now() < Date.parse(pass.expiresAt) || changed.length > 512) fail();
44
+ const changes = [];
45
+ for (const relative of changed) {
46
+ const observed = comparison.files.find(file => file.path === relative);
47
+ if (before.has(relative) || protectedPaths.has(relative) || observed?.kind !== 'file') fail();
48
+ const current = await repositoryIntegrityFile(root, relative), identity = current.identity;
49
+ if (current.sha256 !== observed.sha256 || current.mode !== observed.mode || current.mode & 0o111
50
+ || identity.nlink !== 1 || !Number.isFinite(identity.birthtimeMs) || identity.birthtimeMs <= completedAt
51
+ || identity.birthtimeMs > Date.now() || identity.ctimeMs < identity.birthtimeMs) fail();
52
+ changes.push({ path: relative, before: null, after: { sha256: current.sha256, mode: current.mode },
53
+ physicalIdentity: identity, createdAt: new Date(identity.birthtimeMs).toISOString() });
54
+ }
55
+ return changes;
56
+ }
57
+ export async function observeCompletedMutationEnvironment(input, state, review, writtenLedger) {
58
+ const { comparison, changed } = await observeDisciplineRepositoryState(input, state.binding.root, review, writtenLedger);
59
+ if (!changed.length) return null;
60
+ if (changed.length > 512) fail();
61
+ const completionEvidence = await mutationCompletionEvidence(input, state);
62
+ const finder = changed.filter(finderMetadataPath), other = changed.filter(relative => !finderMetadataPath(relative));
63
+ const changes = [...(finder.length ? await observeFinderMetadataChanges(state.binding.root, review, comparison, finder, state, completionEvidence) : []),
64
+ ...await additions(state.binding.root, review, comparison, other, state, completionEvidence)].sort((a, b) => a.path < b.path ? -1 : 1);
65
+ const pass = state.pass;
66
+ const core = { schemaVersion, keyId: pass.writeDelegation.mutationKeyId, ownerId: pass.ownerId, taskId: pass.taskId,
67
+ flowId: pass.flowId, stepId: pass.stepId, passId: pass.passId, passDigest: sha256(canonical(pass)),
68
+ originalReviewDigest: pass.snapshotReceipt.disciplineReviewDigest,
69
+ originalComparisonDigest: sha256(canonical(review.comparison)), observedComparisonDigest: sha256(canonical(comparison)),
70
+ attribution: attribution(changes), grantsWriteAuthority: false,
71
+ claimsRepositoryUnchanged: false, claimsAuthorizedEnvironmentChange: false, completionEvidence, changes };
72
+ const stateRoot = await mutationStateRoot14(input), file = location(stateRoot, pass);
73
+ let previous;
74
+ try { previous = verified(await readMutationJson14(file), pass); } catch (error) { if (error.code !== 'ENOENT') throw error; }
75
+ if (previous) {
76
+ const { signature: _, observedAt: __, nonce: ___, ...original } = previous;
77
+ if (canonical(original) !== canonical(core)) fail();
78
+ return previous;
79
+ }
80
+ const signer = await loadPreservedMutationSigner(input, state.binding.signerConfigPath, core.keyId);
81
+ const unsigned = { ...core, observedAt: new Date().toISOString(), nonce: randomUUID() };
82
+ const result = { ...unsigned, signature: sign(null, Buffer.from(canonical(unsigned)), signer.privateKey).toString('base64url') };
83
+ await atomicMutationJson14(file, result, { privateFrom: stateRoot });
84
+ return result;
85
+ }
@@ -0,0 +1,58 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { sha256 } from './local-integrity.mjs';
4
+ import { mutationTarget14, mutationFail14 } from './local-mutation-files.mjs';
5
+ import { validateFinderStore } from './local-finder-store.mjs';
6
+
7
+ const fail = () => mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT',
8
+ 'Finder metadata recovery requires bounded valid allocator/tree/property-list bytes, original successful write evidence, unchanged protected sources, and single-link non-executable files changed only after completion');
9
+ const same = (left, right) => ['dev', 'ino', 'nlink', 'size', 'mtimeMs', 'ctimeMs', 'mode'].every(key => left[key] === right[key]);
10
+ export const finderMetadataPath = relative => path.posix.basename(relative) === '.DS_Store';
11
+
12
+ async function metadataFile(root, relative) {
13
+ const item = await mutationTarget14(root, relative), handle = await fs.open(item.target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
14
+ try {
15
+ const before = await handle.stat();
16
+ if (!same(before, item.stat) || !before.isFile() || before.nlink !== 1 || before.mode & 0o7111
17
+ || before.size > 1_048_576) fail();
18
+ const bytes = Buffer.alloc(before.size);
19
+ let offset = 0;
20
+ while (offset < bytes.length) {
21
+ const read = await handle.read(bytes, offset, bytes.length - offset, offset);
22
+ if (!read.bytesRead) fail();
23
+ offset += read.bytesRead;
24
+ }
25
+ let format;
26
+ try { format = validateFinderStore(bytes); } catch { fail(); }
27
+ const after = await handle.stat(), current = await mutationTarget14(root, relative);
28
+ if (!same(before, after) || !same(after, current.stat)) fail();
29
+ return { sha256: sha256(bytes), mode: after.mode & 0o777, format,
30
+ identity: { dev: after.dev, ino: after.ino, nlink: after.nlink, birthtimeMs: after.birthtimeMs,
31
+ ctimeMs: after.ctimeMs, mtimeMs: after.mtimeMs, size: after.size } };
32
+ } finally { await handle.close(); }
33
+ }
34
+ export async function observeFinderMetadataChanges(root, review, comparison, changed, state, completionEvidence) {
35
+ const pass = state.pass, protectedPaths = new Set([...pass.paths, ...pass.writeDelegation.protectedFiles.map(file => file.path)]);
36
+ const completedAt = Math.max(...completionEvidence.map(record => Date.parse(record.completedAt)));
37
+ if (!completionEvidence.length || !Number.isFinite(completedAt) || Date.now() < Date.parse(pass.expiresAt)
38
+ || !changed.length || changed.length > 512) fail();
39
+ const changes = [];
40
+ let bytes = 0;
41
+ for (const relative of changed) {
42
+ const before = review.comparison.files.find(file => file.path === relative) ?? null;
43
+ const observed = comparison.files.find(file => file.path === relative);
44
+ if (!finderMetadataPath(relative) || protectedPaths.has(relative) || observed?.kind !== 'file'
45
+ || before && (before.kind !== 'file' || before.mode !== observed.mode || before.mode & 0o111)) fail();
46
+ const current = await metadataFile(root, relative), identity = current.identity;
47
+ if (current.sha256 !== observed.sha256 || current.mode !== observed.mode
48
+ || ![identity.birthtimeMs, identity.mtimeMs, identity.ctimeMs].every(value => Number.isFinite(value) && value <= Date.now())
49
+ || identity.mtimeMs <= completedAt || identity.ctimeMs <= completedAt
50
+ || !before && identity.birthtimeMs <= completedAt || identity.birthtimeMs > identity.ctimeMs) fail();
51
+ bytes += identity.size;
52
+ if (bytes > 8_388_608) fail();
53
+ changes.push({ path: relative, kind: 'validated-finder-metadata', before,
54
+ after: { sha256: current.sha256, mode: current.mode }, physicalIdentity: identity, format: current.format,
55
+ originalContentAvailable: false, claimsOriginalContentFormatValidated: false });
56
+ }
57
+ return changes;
58
+ }