@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,61 @@
1
+ import { spawn, execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { fail, relativePath } from './local-budget-grants.mjs';
7
+
8
+ const execute = promisify(execFile), oid = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
9
+ const flags = ['--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-c', 'diff.external='];
10
+ function invalid(message) { fail('ENTROROVE_GIT_SOURCE_INVALID', message); }
11
+ export function gitReadExecution(root, deadlineAt, encoding = 'utf8') {
12
+ const remaining = deadlineAt - Date.now();
13
+ if (!Number.isFinite(remaining) || remaining <= 0) fail('ENTROROVE_BUDGET_LEASE_EXPIRED', 'fixed Git source authorization expired');
14
+ return { cwd: root, encoding, timeout: Math.max(1, Math.min(10000, remaining)), maxBuffer: 2097152,
15
+ killSignal: 'SIGKILL', env: { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(?:GIT_|DYLD_|LD_)/.test(key))),
16
+ GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' } };
17
+ }
18
+ export function gitSourceDescriptor(value) {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)
20
+ || Object.keys(value).sort().join('\0') !== ['path', 'bytes', 'objectId', 'mode', 'role', 'commitId', ...(value.sourceKind === 'index' ? ['sourceKind'] : [])].sort().join('\0')) invalid('Git blob descriptor must have its exact fixed source fields');
21
+ const descriptor = structuredClone(value);
22
+ relativePath(descriptor.path);
23
+ if (descriptor.role !== 'git-blob' || !oid.test(descriptor.objectId ?? '') || (descriptor.sourceKind === 'index'
24
+ ? descriptor.commitId !== null : !oid.test(descriptor.commitId ?? '') || descriptor.objectId.length !== descriptor.commitId.length) || !['100644', '100755', '120000'].includes(descriptor.mode)
25
+ || !Number.isSafeInteger(descriptor.bytes) || descriptor.bytes < 0) invalid('Git blob needs its actual commit, original path, object ID, mode and size');
26
+ return Object.freeze(descriptor);
27
+ }
28
+ async function metadata(root, args, deadlineAt) {
29
+ try { return (await execute('/usr/bin/git', [...flags, ...args], gitReadExecution(root, deadlineAt))).stdout; }
30
+ catch (error) {
31
+ if (Date.now() >= deadlineAt || error.code === 'ENTROROVE_BUDGET_LEASE_EXPIRED') fail('ENTROROVE_BUDGET_LEASE_EXPIRED', 'fixed Git object metadata authorization expired');
32
+ fail('ENTROROVE_GIT_FAILED', 'fixed Git object metadata inspection failed');
33
+ }
34
+ }
35
+ export async function verifyFixedGitBlob(root, descriptor, deadlineAt) {
36
+ const stat = await fs.lstat(root);
37
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await fs.realpath(root) !== root) invalid('Git source requires its canonical original repository root');
38
+ if (await fs.realpath((await metadata(root, ['rev-parse', '--show-toplevel'], deadlineAt)).trim()) !== root) invalid('Git source lost its actual original repository');
39
+ if (descriptor.sourceKind === 'index') {
40
+ const index = await metadata(root, ['ls-files', '--stage', '-z', '--', `:(literal)${descriptor.path}`], deadlineAt);
41
+ if (index !== `${descriptor.mode} ${descriptor.objectId} 0\t${descriptor.path}\0`) invalid('Git source differs from its actual original-path stage-zero index blob');
42
+ } else {
43
+ if ((await metadata(root, ['cat-file', '-t', descriptor.commitId], deadlineAt)).trim() !== 'commit') invalid('Git source lost its actual immutable commit');
44
+ const tree = await metadata(root, ['ls-tree', '-z', '--full-tree', descriptor.commitId, '--', `:(literal)${descriptor.path}`], deadlineAt);
45
+ if (tree !== `${descriptor.mode} blob ${descriptor.objectId}\t${descriptor.path}\0`) invalid('Git source path, mode or blob differs from its selected commit');
46
+ }
47
+ if ((await metadata(root, ['cat-file', '-t', descriptor.objectId], deadlineAt)).trim() !== 'blob'
48
+ || (await metadata(root, ['cat-file', '-s', descriptor.objectId], deadlineAt)).trim() !== String(descriptor.bytes)) invalid('Git source exact blob type or byte size differs');
49
+ }
50
+ export function fixedGitBlobStream(root, descriptor, deadlineAt) {
51
+ const child = spawn('/usr/bin/git', [...flags, 'cat-file', 'blob', descriptor.objectId],
52
+ { ...gitReadExecution(root, deadlineAt, null), stdio: ['ignore', 'pipe', 'pipe'] });
53
+ child.stderr.resume();
54
+ const done = new Promise(resolve => { child.once('error', () => resolve(false)); child.once('close', code => resolve(code === 0)); });
55
+ return { chunks: child.stdout, done, close: () => child.kill('SIGKILL') };
56
+ }
57
+ export function verifyGitBlobBytes(descriptor, content) {
58
+ const digest = createHash(descriptor.objectId.length === 40 ? 'sha1' : 'sha256')
59
+ .update(`blob ${content.length}\0`).update(content).digest('hex');
60
+ if (content.length !== descriptor.bytes || digest !== descriptor.objectId) invalid('actual Git source bytes do not match the immutable selected blob and size');
61
+ }
@@ -0,0 +1,81 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { withBudget, loadBudget, commitBudget } from './local-budget-store.mjs';
3
+ import { consumeBudgetRenewal } from './local-budget-renewal.mjs';
4
+ import { assertBudgetReadContext, assertDependencyReadAllowed } from './local-budget-reads.mjs';
5
+ import { fail, requireLiveBudget } from './local-budget-grants.mjs';
6
+ import { validateGitRead } from './local-budget-git.mjs';
7
+ import { fixedNativeRequestStore } from './local-budget-git-native-request.mjs';
8
+ import { assertGitScratchSessionInput, assertGitScratch } from './local-budget-git-scratch-loader.mjs';
9
+ const requests = fixedNativeRequestStore('attributes');
10
+ export async function createFixedNativeAttributeRequest(input, session, request) {
11
+ await assertGitScratchSessionInput(input, session);
12
+ if (!session.attributeDescriptors.length) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'derived native reading requires genuine source segments');
13
+ return requests.issue(input, session, session.attributeDescriptors[0], request);
14
+ }
15
+ function parts(session, request, length, prior) {
16
+ const grouped = new Map();
17
+ for (const descriptor of session.attributeDescriptors) {
18
+ const start = Math.max(descriptor.start, request.offset), end = Math.min(descriptor.end, request.offset + length);
19
+ if (end <= start) continue;
20
+ const part = grouped.get(descriptor.path) ?? { descriptor, bytes: 0, prior: prior.derived?.[descriptor.path] ?? 0, started: Object.hasOwn(prior.derived ?? {}, descriptor.path) };
21
+ part.bytes += end - start; grouped.set(descriptor.path, part);
22
+ }
23
+ return [...grouped.values()].map(part => ({ ...part, tokens: Math.ceil((part.prior + part.bytes) / 4) - Math.ceil(part.prior / 4) }));
24
+ }
25
+ function charge(state, charged) {
26
+ const files = [...state.files];
27
+ for (const part of charged) if (!files.includes(part.descriptor.path)) files.push(part.descriptor.path);
28
+ return { ...state, files, readCalls: state.readCalls + charged.filter(part => !part.started).length,
29
+ tokenEstimate: state.tokenEstimate + charged.reduce((n, part) => n + part.tokens, 0) };
30
+ }
31
+ async function readActual(session, request, received) {
32
+ for (const descriptor of session.attributeDescriptors) await assertGitScratch(descriptor);
33
+ const handle = await fs.open(session.attributes, constants.O_RDONLY | constants.O_NOFOLLOW);
34
+ try {
35
+ const stat = await handle.stat({ bigint: true });
36
+ if (!stat.isFile() || stat.ino !== session.attributeStat.ino || stat.dev !== session.attributeStat.dev) fail('ENTROROVE_GIT_SOURCE_INVALID', 'derived Git attribute cache changed its actual inode');
37
+ const capacity = Math.min(request.count, Math.max(0, Number(stat.size) - request.offset)), buffer = Buffer.alloc(capacity);
38
+ const { bytesRead } = await handle.read(buffer, 0, capacity, request.offset);
39
+ received.actual = buffer.subarray(0, bytesRead); return received.actual;
40
+ } finally { await handle.close(); }
41
+ }
42
+ export async function readFixedNativeAttributeChunk(input, session, receipt) {
43
+ await assertGitScratchSessionInput(input, session);
44
+ const { request, prior, readId } = await requests.consume(input, session.attributeDescriptors[0], receipt);
45
+ try { const result = await withBudget(input, async (location, loaded) => {
46
+ let state = loaded.state, committed = false; const received = { actual: null };
47
+ if (state.grant?.purpose === 'handoff-read') requireLiveBudget(state); else state = consumeBudgetRenewal({ state });
48
+ const event = charged => ({ readId, path: charged[0]?.descriptor.path ?? null,
49
+ bytes: charged.reduce((n, part) => n + part.bytes, 0), actualCacheBytes: received.actual.length,
50
+ tokens: charged.reduce((n, part) => n + part.tokens, 0), source: { role: 'git-process-derived',
51
+ native: request, segments: charged.map(part => ({ path: part.descriptor.path, sourceSha256: part.descriptor.sha256,
52
+ bytes: part.bytes, tokens: part.tokens, start: part.descriptor.start, end: part.descriptor.end })) } });
53
+ try {
54
+ await assertBudgetReadContext(input); await assertDependencyReadAllowed(input);
55
+ const expected = parts(session, request, request.count, prior), cap = expected.reduce((n, part) => n + part.tokens, 0);
56
+ for (const part of expected) await validateGitRead(input, state, part.descriptor, cap * 4);
57
+ const actual = await readActual(session, request, received), charged = parts(session, request, actual.length, prior);
58
+ for (const part of charged) { await assertGitScratch(part.descriptor); await validateGitRead(input, state, part.descriptor, cap * 4); }
59
+ if (charged.length) {
60
+ state = await commitBudget(location, loaded, charge(state, charged), 'budget-read', event(charged)); committed = true;
61
+ }
62
+ for (const part of charged) await validateGitRead(input, state, part.descriptor);
63
+ await assertBudgetReadContext(input);
64
+ return { actual, derived: { ...prior.derived, ...Object.fromEntries(charged.map(part => [part.descriptor.path, part.prior + part.bytes])) } };
65
+ } catch (error) {
66
+ let current = await loadBudget(location);
67
+ if (current.latest.data.readId === readId) committed = true;
68
+ if (!committed && received.actual !== null) {
69
+ const charged = parts(session, request, received.actual.length, prior);
70
+ if (charged.length) await commitBudget(location, current, charge(current.state, charged), 'budget-read', { ...event(charged), readIncomplete: true, notDelivered: true });
71
+ }
72
+ current = await loadBudget(location);
73
+ if (error.code?.startsWith('ENTROROVE_') && error.code !== 'ENTROROVE_BUDGET_CHAIN_INVALID') {
74
+ await commitBudget(location, current, current.state, 'budget-read-denied', { sourceRole: 'git-process-derived', code: error.code });
75
+ }
76
+ throw error;
77
+ }
78
+ });
79
+ requests.complete(receipt, result.actual.length, result.derived); return result;
80
+ } catch (error) { requests.failed(receipt); throw error; }
81
+ }
@@ -0,0 +1,104 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { createHash } from 'node:crypto';
7
+ import { fail } from './local-budget-grants.mjs';
8
+ import { darwinGitNativeArtifacts } from './runtime/git-native-darwin-v1.mjs';
9
+ const execute = promisify(execFile), hash = data => createHash('sha256').update(data).digest('hex');
10
+ const sourceSHA = 'ae866387813c965acf6316733813be45f75d3df8d6f0f8afc2228cfcbdd4e830';
11
+ const runtimes = new WeakMap(), builds = new Map(), auxiliaryRoots = new Map();
12
+ const unsupported = () => fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Darwin Git metering requires its actual root-owned Git, pinned native artifact and fixed codesign tool');
13
+ const environment = () => ({ PATH: '/usr/bin:/bin', HOME: '/var/empty', LANG: 'C', LC_ALL: 'C', TMPDIR: '/tmp' });
14
+ async function directory(target, create = false, owner = process.getuid(), privateMode = true) {
15
+ if (create) await fs.mkdir(target, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
16
+ const stat = await fs.lstat(target);
17
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== owner || stat.mode & (privateMode ? 0o7077 : 0o7022)
18
+ || await fs.realpath(target) !== target) unsupported();
19
+ }
20
+ async function identity(target, owner, privateMode = false, expected) {
21
+ const stat = await fs.lstat(target, { bigint: true });
22
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== BigInt(owner) || stat.mode & BigInt(privateMode ? 0o7077 : 0o7022)
23
+ || await fs.realpath(target) !== target) unsupported();
24
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
25
+ let sha;
26
+ try {
27
+ const opened = await handle.stat({ bigint: true });
28
+ if (opened.dev !== stat.dev || opened.ino !== stat.ino) unsupported();
29
+ sha = hash(await handle.readFile()); const after = await handle.stat({ bigint: true });
30
+ if (after.ctimeNs !== stat.ctimeNs || after.mtimeNs !== stat.mtimeNs || expected && sha !== expected) unsupported();
31
+ } finally { await handle.close(); }
32
+ return sha;
33
+ }
34
+ async function trustedTools() {
35
+ const xcrunSHA = await identity('/usr/bin/xcrun', 0), codesignSHA = await identity('/usr/bin/codesign', 0);
36
+ const result = await execute('/usr/bin/xcrun', ['--find', 'git'], { env: environment(), timeout: 5000, maxBuffer: 8192 });
37
+ await identity('/usr/bin/xcrun', 0, false, xcrunSHA);
38
+ const git = result.stdout.trim();
39
+ if (result.stderr || !path.isAbsolute(git) || git.includes('\n')
40
+ || !(git === '/Library/Developer/CommandLineTools/usr/bin/git'
41
+ || /^\/Applications\/[A-Za-z0-9._ -]+\.app\/Contents\/Developer\/usr\/bin\/git$/.test(git))) unsupported();
42
+ for (let target = path.dirname(git); target !== '/'; target = path.dirname(target)) await directory(target, false, 0, false);
43
+ return { git, gitSHA: await identity(git, 0), codesignSHA };
44
+ }
45
+ async function artifactState(home) {
46
+ if (!/[\s:]/.test(home)) {
47
+ const state = path.join(home, '.entrorove'); await directory(state, true); return state;
48
+ }
49
+ let pending = auxiliaryRoots.get(home);
50
+ if (!pending) {
51
+ pending = (async () => {
52
+ const temporary = await fs.realpath('/tmp'), stat = await fs.lstat(temporary);
53
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0 || !(stat.mode & 0o1000)) unsupported();
54
+ const state = await fs.mkdtemp(path.join(temporary, `entrorove-fixed-git-${process.getuid()}-${hash(home).slice(0, 16)}-`));
55
+ await fs.chmod(state, 0o700); await directory(state); return state;
56
+ })();
57
+ auxiliaryRoots.set(home, pending); pending.catch(() => auxiliaryRoots.delete(home));
58
+ }
59
+ return pending;
60
+ }
61
+ async function build(home, tools, artifact) {
62
+ const state = await artifactState(home);
63
+ const parent = path.join(state, 'fixed-git'); await directory(parent, true);
64
+ const profile = hash(JSON.stringify({ home, ...tools, sourceSHA, librarySHA: artifact.sha256, arch: process.arch }));
65
+ const destination = path.join(parent, profile), staging = await fs.mkdtemp(path.join(parent, '.building-'));
66
+ try {
67
+ await fs.chmod(staging, 0o700);
68
+ const library = path.join(staging, 'entrorove-fixed-git.dylib'), bytes = Buffer.from(artifact.base64, 'base64');
69
+ if (hash(bytes) !== artifact.sha256 || !['cffaedfe', 'cefaedfe'].includes(bytes.subarray(0, 4).toString('hex'))) unsupported();
70
+ await fs.writeFile(library, bytes, { mode: 0o600, flag: 'wx' });
71
+ const librarySHA = await identity(library, process.getuid(), true, artifact.sha256);
72
+ await identity(tools.git, 0, false, tools.gitSHA); await identity('/usr/bin/codesign', 0, false, tools.codesignSHA);
73
+ const git = path.join(staging, 'entrorove-fixed-git');
74
+ await fs.copyFile(tools.git, git, constants.COPYFILE_EXCL);
75
+ await identity(git, process.getuid(), false, tools.gitSHA);
76
+ await execute('/usr/bin/codesign', ['--remove-signature', git], { env: environment(), timeout: 5000, maxBuffer: 8192 });
77
+ await execute('/usr/bin/codesign', ['--force', '--sign', '-', '--identifier', 'entrorove.fixed-git', git], { env: environment(), timeout: 5000, maxBuffer: 8192 });
78
+ await fs.chmod(git, 0o700);
79
+ const copySHA = await identity(git, process.getuid(), true);
80
+ await identity(tools.git, 0, false, tools.gitSHA); await identity('/usr/bin/codesign', 0, false, tools.codesignSHA);
81
+ await fs.rename(staging, destination).catch(error => { if (!['EEXIST', 'ENOTEMPTY'].includes(error.code)) throw error; });
82
+ const runtime = Object.freeze({ directory: destination, git: path.join(destination, 'entrorove-fixed-git'), library: path.join(destination, 'entrorove-fixed-git.dylib'),
83
+ loaderEnv: Object.freeze({ DYLD_INSERT_LIBRARIES: path.join(destination, 'entrorove-fixed-git.dylib') }), platform: 'darwin' });
84
+ runtimes.set(runtime, { ...tools, librarySHA, copySHA }); await assertDarwinGitRuntime(runtime); return runtime;
85
+ } finally { await fs.rm(staging, { recursive: true, force: true }); }
86
+ }
87
+ export async function darwinGitNativeRuntime(input) {
88
+ try {
89
+ if (process.platform !== 'darwin' || !['arm64', 'x64'].includes(process.arch) || !input.home) unsupported();
90
+ const home = await fs.realpath(input.home);
91
+ const source = await fs.readFile(fileURLToPath(new URL('./local-budget-git-native.c', import.meta.url))); if (hash(source) !== sourceSHA) unsupported();
92
+ const artifact = darwinGitNativeArtifacts[process.arch]; if (!artifact || artifact.sourceSHA !== sourceSHA) unsupported();
93
+ const tools = await trustedTools(), key = JSON.stringify({ home, ...tools, sourceSHA, librarySHA: artifact.sha256 });
94
+ let pending = builds.get(key);
95
+ if (!pending) { pending = build(home, tools, artifact); builds.set(key, pending); pending.catch(() => builds.delete(key)); }
96
+ const runtime = await pending; await assertDarwinGitRuntime(runtime); return runtime;
97
+ } catch (error) { if (error.code?.startsWith('ENTROROVE_')) throw error; unsupported(); }
98
+ }
99
+ export async function assertDarwinGitRuntime(runtime) {
100
+ const expected = runtimes.get(runtime); if (!expected) unsupported();
101
+ await directory(runtime.directory);
102
+ await identity(expected.git, 0, false, expected.gitSHA); await identity('/usr/bin/codesign', 0, false, expected.codesignSHA);
103
+ await identity(runtime.git, process.getuid(), true, expected.copySHA); await identity(runtime.library, process.getuid(), true, expected.librarySHA);
104
+ }
@@ -0,0 +1,93 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createHash } from 'node:crypto';
5
+ import { fail } from './local-budget-grants.mjs';
6
+ import { linuxGitNativeArtifacts } from './runtime/git-native-linux-v1.mjs';
7
+ const hash = data => createHash('sha256').update(data).digest('hex');
8
+ const sourceSHA = 'ae866387813c965acf6316733813be45f75d3df8d6f0f8afc2228cfcbdd4e830';
9
+ const runtimes = new WeakMap(), builds = new Map(), auxiliaryRoots = new Map();
10
+ const unsupported = () => fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Linux Git metering requires its actual root-owned Git, pinned native artifact and glibc runtime');
11
+ async function directory(target, create = false) {
12
+ if (create) await fs.mkdir(target, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
13
+ const stat = await fs.lstat(target);
14
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid() || stat.mode & 0o077 || await fs.realpath(target) !== target) unsupported();
15
+ }
16
+ async function identity(target, owner, privateMode = false, expected) {
17
+ const stat = await fs.lstat(target, { bigint: true });
18
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== BigInt(owner) || stat.mode & BigInt(privateMode ? 0o7077 : 0o7022)
19
+ || await fs.realpath(target) !== target) unsupported();
20
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
21
+ let sha;
22
+ try {
23
+ const opened = await handle.stat({ bigint: true });
24
+ if (opened.dev !== stat.dev || opened.ino !== stat.ino) unsupported();
25
+ sha = hash(await handle.readFile()); const after = await handle.stat({ bigint: true });
26
+ if (after.ctimeNs !== stat.ctimeNs || after.mtimeNs !== stat.mtimeNs || expected && sha !== expected) unsupported();
27
+ } finally { await handle.close(); }
28
+ return sha;
29
+ }
30
+ async function trustedGit() {
31
+ if (await fs.realpath('/usr/bin/git') !== '/usr/bin/git') unsupported();
32
+ for (const target of ['/usr', '/usr/bin']) {
33
+ const stat = await fs.lstat(target);
34
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0 || stat.mode & 0o022) unsupported();
35
+ }
36
+ return { git: '/usr/bin/git', gitSHA: await identity('/usr/bin/git', 0) };
37
+ }
38
+ async function artifactState(home) {
39
+ if (!/[\s:]/.test(home)) {
40
+ const state = path.join(home, '.entrorove'); await directory(state, true); return state;
41
+ }
42
+ let pending = auxiliaryRoots.get(home);
43
+ if (!pending) {
44
+ pending = (async () => {
45
+ const temporary = await fs.realpath('/tmp'), stat = await fs.lstat(temporary);
46
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0 || !(stat.mode & 0o1000)) unsupported();
47
+ const state = await fs.mkdtemp(path.join(temporary, `entrorove-fixed-git-${process.getuid()}-${hash(home).slice(0, 16)}-`));
48
+ await fs.chmod(state, 0o700); await directory(state); return state;
49
+ })();
50
+ auxiliaryRoots.set(home, pending); pending.catch(() => auxiliaryRoots.delete(home));
51
+ }
52
+ return pending;
53
+ }
54
+ async function build(home, tools, artifact) {
55
+ const state = await artifactState(home);
56
+ const parent = path.join(state, 'fixed-git-linux'); await directory(parent, true);
57
+ const profile = hash(JSON.stringify({ home, ...tools, sourceSHA, librarySHA: artifact.sha256, arch: process.arch }));
58
+ const destination = path.join(parent, profile), staging = await fs.mkdtemp(path.join(parent, '.building-'));
59
+ try {
60
+ await fs.chmod(staging, 0o700);
61
+ const library = path.join(staging, 'entrorove-fixed-git.so'), bytes = Buffer.from(artifact.base64, 'base64');
62
+ if (hash(bytes) !== artifact.sha256 || bytes.subarray(0, 4).toString('hex') !== '7f454c46') unsupported();
63
+ await fs.writeFile(library, bytes, { mode: 0o600, flag: 'wx' });
64
+ const librarySHA = await identity(library, process.getuid(), true, artifact.sha256);
65
+ await identity(tools.git, 0, false, tools.gitSHA);
66
+ const git = path.join(staging, 'entrorove-fixed-git');
67
+ await fs.copyFile(tools.git, git, constants.COPYFILE_EXCL); await fs.chmod(git, 0o700);
68
+ await identity(git, process.getuid(), true, tools.gitSHA);
69
+ await fs.rename(staging, destination).catch(error => { if (!['EEXIST', 'ENOTEMPTY'].includes(error.code)) throw error; });
70
+ const runtime = Object.freeze({ directory: destination, git: path.join(destination, 'entrorove-fixed-git'), library: path.join(destination, 'entrorove-fixed-git.so'),
71
+ loaderEnv: Object.freeze({ LD_PRELOAD: path.join(destination, 'entrorove-fixed-git.so') }), platform: 'linux' });
72
+ runtimes.set(runtime, { ...tools, librarySHA }); await assertLinuxGitRuntime(runtime); return runtime;
73
+ } finally { await fs.rm(staging, { recursive: true, force: true }); }
74
+ }
75
+ export async function linuxGitNativeRuntime(input) {
76
+ try {
77
+ if (process.platform !== 'linux' || !['arm64', 'x64'].includes(process.arch) || !process.report.getReport().header.glibcVersionRuntime || !input.home) unsupported();
78
+ const home = await fs.realpath(input.home);
79
+ const source = await fs.readFile(fileURLToPath(new URL('./local-budget-git-native.c', import.meta.url))); if (hash(source) !== sourceSHA) unsupported();
80
+ const artifact = linuxGitNativeArtifacts[process.arch];
81
+ if (!artifact || artifact.sourceSHA !== sourceSHA) unsupported();
82
+ const tools = await trustedGit(), key = JSON.stringify({ home, ...tools, sourceSHA, librarySHA: artifact.sha256 });
83
+ let pending = builds.get(key);
84
+ if (!pending) { pending = build(home, tools, artifact); builds.set(key, pending); pending.catch(() => builds.delete(key)); }
85
+ const runtime = await pending; await assertLinuxGitRuntime(runtime); return runtime;
86
+ } catch (error) { if (error.code?.startsWith('ENTROROVE_')) throw error; unsupported(); }
87
+ }
88
+ export async function assertLinuxGitRuntime(runtime) {
89
+ const expected = runtimes.get(runtime); if (!expected) unsupported();
90
+ await directory(runtime.directory);
91
+ await identity(expected.git, 0, false, expected.gitSHA);
92
+ await identity(runtime.git, process.getuid(), true, expected.gitSHA); await identity(runtime.library, process.getuid(), true, expected.librarySHA);
93
+ }
@@ -0,0 +1,88 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { withBudget, loadBudget, commitBudget } from './local-budget-store.mjs';
3
+ import { assertBudgetReadContext, assertDependencyReadAllowed } from './local-budget-reads.mjs';
4
+ import { consumeBudgetRenewal } from './local-budget-renewal.mjs';
5
+ import { requireLiveBudget, fail } from './local-budget-grants.mjs';
6
+ import { validateGitRead } from './local-budget-git.mjs';
7
+ import { fixedNativeRequestStore } from './local-budget-git-native-request.mjs';
8
+ import { assertGitScratchInput, assertGitScratchSessionInput, assertGitScratch, verifyNativeScratchChunk } from './local-budget-git-scratch-loader.mjs';
9
+
10
+ const requests = fixedNativeRequestStore('source');
11
+ export const createFixedNativeGitRequest = (input, session, descriptor, request) => requests.issue(input, session, descriptor, request);
12
+ function renewed(state) {
13
+ if (state.grant?.purpose === 'handoff-read') { requireLiveBudget(state); return state; }
14
+ return consumeBudgetRenewal({ state });
15
+ }
16
+ async function actualBytes(descriptor, request, received) {
17
+ await assertGitScratch(descriptor);
18
+ if (request.kind === 'readlink') {
19
+ if (descriptor.mode !== '120000' || request.offset || request.count < descriptor.bytes) fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git requested an invalid original symlink');
20
+ received.actual = Buffer.from(await fs.readlink(descriptor.target)); return received.actual;
21
+ }
22
+ if (descriptor.mode === '120000') fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git cannot dereference an original symlink source');
23
+ const handle = await fs.open(descriptor.target, constants.O_RDONLY | constants.O_NOFOLLOW);
24
+ try {
25
+ const stat = await handle.stat({ bigint: true });
26
+ if (!stat.isFile() || stat.ino !== descriptor.stat.ino || stat.dev !== descriptor.stat.dev) fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git source descriptor changed');
27
+ const capacity = Math.min(request.count, Math.max(0, descriptor.bytes - request.offset));
28
+ const buffer = Buffer.alloc(capacity), result = await handle.read(buffer, 0, capacity, request.offset);
29
+ received.actual = buffer.subarray(0, result.bytesRead); return received.actual;
30
+ } finally { await handle.close(); }
31
+ }
32
+ function charge(state, descriptor, bytes, prior) {
33
+ const tokens = Math.ceil((prior.bytes + bytes) / 4) - Math.ceil(prior.bytes / 4);
34
+ return { ...state, files: state.files.includes(descriptor.path) ? state.files : [...state.files, descriptor.path],
35
+ readCalls: state.readCalls + (prior.started ? 0 : 1), tokenEstimate: state.tokenEstimate + tokens };
36
+ }
37
+ export async function readFixedNativeGitChunk(input, descriptor, receipt) {
38
+ await assertGitScratchInput(input, descriptor);
39
+ const { request, prior, readId } = await requests.consume(input, descriptor, receipt);
40
+ try { const actual = await withBudget(input, async (location, loaded) => {
41
+ let state = renewed(loaded.state), actual = null, committed = false; const received = { actual: null };
42
+ const event = bytes => ({ path: descriptor.path, bytes,
43
+ tokens: Math.ceil((prior.bytes + bytes) / 4) - Math.ceil(prior.bytes / 4), readId,
44
+ source: { path: descriptor.path, role: 'git-process', bytes: descriptor.bytes, sha256: descriptor.sha256,
45
+ objectId: descriptor.objectId, commitId: descriptor.commitId, mode: descriptor.mode,
46
+ native: { kind: request.kind, operationId: request.id, offset: request.offset, count: request.count, index: request.index } } });
47
+ try {
48
+ await assertBudgetReadContext(input); await assertDependencyReadAllowed(input);
49
+ await validateGitRead(input, state, descriptor);
50
+ const expected = Math.min(request.count, Math.max(0, descriptor.bytes - request.offset));
51
+ const expectedTokens = Math.ceil((prior.bytes + expected) / 4) - Math.ceil(prior.bytes / 4);
52
+ await validateGitRead(input, state, descriptor, expectedTokens * 4);
53
+ actual = await actualBytes(descriptor, request, received);
54
+ verifyNativeScratchChunk(descriptor, request.offset, actual);
55
+ const tokens = Math.ceil((prior.bytes + actual.length) / 4) - Math.ceil(prior.bytes / 4);
56
+ await assertGitScratch(descriptor); await validateGitRead(input, state, descriptor, tokens * 4);
57
+ if (request.kind === 'mmap' && actual.length !== request.count) fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git mapping exceeded actual source bytes');
58
+ state = await commitBudget(location, loaded, charge(state, descriptor, actual.length, prior), 'budget-read', event(actual.length));
59
+ committed = true;
60
+ await validateGitRead(input, state, descriptor); await assertGitScratch(descriptor); await assertBudgetReadContext(input);
61
+ return actual;
62
+ } catch (error) {
63
+ actual ??= received.actual;
64
+ let current = await loadBudget(location);
65
+ if (current.latest.data.readId === readId) committed = true;
66
+ if (!committed && actual !== null) {
67
+ await commitBudget(location, current, charge(current.state, descriptor, actual.length, prior), 'budget-read',
68
+ { ...event(actual.length), readIncomplete: true, notDelivered: true });
69
+ }
70
+ current = await loadBudget(location);
71
+ if (error.code?.startsWith('ENTROROVE_') && error.code !== 'ENTROROVE_BUDGET_CHAIN_INVALID') {
72
+ await commitBudget(location, current, current.state, 'budget-read-denied', { path: descriptor.path, sourceRole: 'git-process', code: error.code });
73
+ }
74
+ throw error;
75
+ }
76
+ });
77
+ requests.complete(receipt, actual.length); return actual;
78
+ } catch (error) { requests.failed(receipt); throw error; }
79
+ }
80
+ export async function assertFixedNativeGitSources(input, session) {
81
+ await assertGitScratchSessionInput(input, session);
82
+ return withBudget(input, async (_location, loaded) => {
83
+ const state = renewed(loaded.state);
84
+ await assertBudgetReadContext(input); await assertDependencyReadAllowed(input);
85
+ for (const descriptor of [...session.descriptors, ...session.attributeDescriptors]) { await assertGitScratchInput(input, descriptor); await validateGitRead(input, state, descriptor); }
86
+ return state;
87
+ });
88
+ }
@@ -0,0 +1,53 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { fail } from './local-budget-grants.mjs';
3
+ import { assertGitScratchInput, assertGitScratchSessionInput } from './local-budget-git-scratch-loader.mjs';
4
+ const invalid = () => fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed native reading requires a single-use adapter request receipt');
5
+ function validated(value, descriptor) {
6
+ if (!value || Object.keys(value).sort().join(',') !== 'count,id,index,kind,length,offset'
7
+ || !['read', 'pread', 'mmap', 'readlink', 'stdio'].includes(value.kind)
8
+ || ['id', 'offset', 'count', 'length', 'index'].some(key => !Number.isSafeInteger(value[key]) || value[key] < 0)
9
+ || value.id < 1 || value.count < 1 || value.count > 65536 || value.length < value.count || value.length > 2147483647
10
+ || value.kind === 'readlink' && value.offset || value.kind === 'mmap' && value.offset + value.count > descriptor.bytes) invalid();
11
+ if (value.kind === 'readlink' && (descriptor.mode !== '120000' || value.count < descriptor.bytes)) {
12
+ fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed readlink must receive all original symlink bytes before charging');
13
+ }
14
+ return Object.freeze({ ...value });
15
+ }
16
+ // Each reader owns a private instance. Callers cannot construct receipts or edit actual completed-byte progress.
17
+ export function fixedNativeRequestStore(role) {
18
+ if (!['source', 'attributes'].includes(role)) invalid();
19
+ const receipts = new WeakMap(), sessions = new WeakMap();
20
+ const issue = async (input, session, descriptor, value) => {
21
+ await assertGitScratchSessionInput(input, session); await assertGitScratchInput(input, descriptor);
22
+ if (!(role === 'source' ? session.descriptors : session.attributeDescriptors).includes(descriptor)) invalid();
23
+ const request = validated(value, descriptor);
24
+ let entry = sessions.get(session);
25
+ if (!entry) { entry = { id: randomUUID(), operations: new Map() }; sessions.set(session, entry); }
26
+ let progress = entry.operations.get(request.id);
27
+ if (!progress) {
28
+ if (request.index !== 0 || request.kind === 'mmap' && request.offset + request.length > descriptor.bytes) invalid();
29
+ progress = { bytes: 0, started: false, index: 0, offset: request.offset, length: request.length, descriptor, kind: request.kind };
30
+ entry.operations.set(request.id, progress);
31
+ }
32
+ if (progress.pending || progress.failed || progress.index !== request.index || progress.descriptor !== descriptor
33
+ || progress.kind !== request.kind || progress.length !== request.length || request.offset !== progress.offset + progress.bytes
34
+ || request.count !== Math.min(65536, progress.length - progress.bytes)) invalid();
35
+ const receipt = Object.freeze({ kind: 'entrorove.fixed-native-read-receipt/1' }); progress.pending = true;
36
+ receipts.set(receipt, { session, descriptor, request, progress, used: false, readId: `${entry.id}:${request.id}:${request.index}` });
37
+ return receipt;
38
+ };
39
+ const consume = async (input, descriptor, receipt) => {
40
+ const entry = receipts.get(receipt);
41
+ if (!entry || entry.used || entry.descriptor !== descriptor) invalid();
42
+ await assertGitScratchSessionInput(input, entry.session); await assertGitScratchInput(input, descriptor);
43
+ entry.used = true;
44
+ return { request: entry.request, prior: structuredClone({ bytes: entry.progress.bytes, started: entry.progress.started, derived: entry.progress.derived }), readId: entry.readId };
45
+ };
46
+ const complete = (receipt, length, derived) => {
47
+ const entry = receipts.get(receipt); if (!entry?.used) invalid();
48
+ entry.progress.bytes += length; entry.progress.started = true; entry.progress.index++;
49
+ entry.progress.derived = derived; entry.progress.pending = false;
50
+ };
51
+ const failed = receipt => { const entry = receipts.get(receipt); if (entry) entry.progress.failed = true; };
52
+ return Object.freeze({ issue, consume, complete, failed });
53
+ }
@@ -0,0 +1,18 @@
1
+ import { linuxGitNativeRuntime, assertLinuxGitRuntime } from './local-budget-git-native-linux.mjs';
2
+ import { darwinGitNativeRuntime, assertDarwinGitRuntime } from './local-budget-git-native-darwin.mjs';
3
+ import { fail } from './local-budget-grants.mjs';
4
+ export function fixedGitEnvironment() {
5
+ return { PATH: '/usr/bin:/bin', HOME: '/var/empty', LANG: 'C', LC_ALL: 'C', TMPDIR: '/tmp',
6
+ GIT_ATTR_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', GIT_CONFIG_NOSYSTEM: '1',
7
+ GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' };
8
+ }
9
+ export async function fixedGitNativeRuntime(input) {
10
+ if (process.platform === 'linux') return linuxGitNativeRuntime(input);
11
+ if (process.platform === 'darwin') return darwinGitNativeRuntime(input);
12
+ fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git metering requires its verified Linux or Darwin artifact');
13
+ }
14
+ export async function assertFixedGitRuntime(runtime) {
15
+ if (runtime?.platform === 'linux') return assertLinuxGitRuntime(runtime);
16
+ if (runtime?.platform === 'darwin') return assertDarwinGitRuntime(runtime);
17
+ fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git runtime identity is invalid');
18
+ }