@entrorove/skill 0.1.23 → 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 +5 -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 +17 -82
  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 +6 -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,313 @@
1
+ #ifdef __linux__
2
+ #define _GNU_SOURCE
3
+ #endif
4
+ #include <unistd.h>
5
+ #include <fcntl.h>
6
+ #include <sys/mman.h>
7
+ #include <sys/stat.h>
8
+ #include <stdlib.h>
9
+ #include <stdio.h>
10
+ #include <string.h>
11
+ #include <errno.h>
12
+ #include <stdint.h>
13
+ #include <limits.h>
14
+ #include <stdatomic.h>
15
+
16
+ #ifdef __linux__
17
+ #include <dlfcn.h>
18
+ static ssize_t (*next_read)(int, void *, size_t);
19
+ static ssize_t (*next_pread)(int, void *, size_t, off_t);
20
+ static ssize_t (*next_readlink)(const char *, char *, size_t);
21
+ static ssize_t (*next_readlinkat)(int, const char *, char *, size_t);
22
+ static void *(*next_mmap)(void *, size_t, int, int, int, off_t);
23
+ static int (*next_munmap)(void *, size_t);
24
+ static FILE *(*next_fopen)(const char *, const char *);
25
+ static FILE *(*next_fdopen)(int, const char *);
26
+ static int (*next_fclose)(FILE *);
27
+ static int (*next_fileno)(FILE *);
28
+ __attribute__((constructor(101))) static void original_functions(void) {
29
+ next_read = dlsym(RTLD_NEXT, "read"); next_pread = dlsym(RTLD_NEXT, "pread");
30
+ next_readlink = dlsym(RTLD_NEXT, "readlink"); next_readlinkat = dlsym(RTLD_NEXT, "readlinkat");
31
+ next_mmap = dlsym(RTLD_NEXT, "mmap"); next_munmap = dlsym(RTLD_NEXT, "munmap");
32
+ next_fopen = dlsym(RTLD_NEXT, "fopen"); next_fdopen = dlsym(RTLD_NEXT, "fdopen");
33
+ next_fclose = dlsym(RTLD_NEXT, "fclose"); next_fileno = dlsym(RTLD_NEXT, "fileno");
34
+ if (!next_read || !next_pread || !next_readlink || !next_readlinkat || !next_mmap || !next_munmap
35
+ || !next_fopen || !next_fdopen || !next_fclose || !next_fileno) _exit(199);
36
+ }
37
+ #define read next_read
38
+ #define pread next_pread
39
+ #define readlink next_readlink
40
+ #define mmap next_mmap
41
+ #define munmap next_munmap
42
+ #define fopen next_fopen
43
+ #define fdopen next_fdopen
44
+ #define fclose next_fclose
45
+ #define fileno next_fileno
46
+ #define INTERPOSE(replacement, original)
47
+ #else
48
+ #define INTERPOSE(replacement, original) \
49
+ __attribute__((used)) static struct { const void *next; const void *prior; } \
50
+ hook_##original __attribute__((section("__DATA,__interpose"))) = \
51
+ { (const void *)&replacement, (const void *)&original };
52
+ #endif
53
+ #define CHUNK 65536
54
+ #define MAX_MAPPING 2147483647
55
+ static atomic_uint_fast64_t sequence;
56
+ struct mapping { void *address; size_t length; };
57
+ static struct mapping mappings[512];
58
+
59
+ static int write_exact(int fd, const void *buffer, size_t length) {
60
+ size_t sent = 0;
61
+ while (sent < length) {
62
+ ssize_t n = write(fd, (const char *)buffer + sent, length - sent);
63
+ if (n < 0 && errno == EINTR) continue;
64
+ if (n <= 0) return -1;
65
+ sent += (size_t)n;
66
+ }
67
+ return 0;
68
+ }
69
+ static int read_exact(int fd, void *buffer, size_t length) {
70
+ size_t received = 0;
71
+ while (received < length) {
72
+ ssize_t n = read(fd, (char *)buffer + received, length - received);
73
+ if (n < 0 && errno == EINTR) continue;
74
+ if (n <= 0) return -1;
75
+ received += (size_t)n;
76
+ }
77
+ return 0;
78
+ }
79
+ static int under_root(const char *target) {
80
+ const char *root = getenv("ENTROROVE_FIXED_GIT_ROOT");
81
+ size_t length = root ? strlen(root) : 0;
82
+ return root && strncmp(target, root, length) == 0 && target[length] == '/';
83
+ }
84
+ static int tracked(int fd, char *target) {
85
+ #ifdef __linux__
86
+ char file[64]; snprintf(file, sizeof(file), "/proc/self/fd/%d", fd);
87
+ ssize_t length = readlink(file, target, PATH_MAX - 1);
88
+ if (length < 0 || length >= PATH_MAX - 1) return 0;
89
+ target[length] = 0; return under_root(target);
90
+ #else
91
+ return fcntl(fd, F_GETPATH, target) == 0 && under_root(target);
92
+ #endif
93
+ }
94
+ static ssize_t supply(const char *kind, const char *target, void *buffer, size_t length, off_t offset) {
95
+ char request[PATH_MAX + 160];
96
+ if (!length) return 0;
97
+ uint64_t read_id = atomic_fetch_add(&sequence, 1) + 1;
98
+ size_t received = 0, chunk_index = 0;
99
+ if (offset < 0 || length > MAX_MAPPING) { errno = EACCES; return -1; }
100
+ while (received < length) {
101
+ size_t ask = length - received > CHUNK ? CHUNK : length - received;
102
+ int n = snprintf(request, sizeof(request), "R\t%llu\t%s\t%s\t%lld\t%zu\t%zu\t%zu\n",
103
+ (unsigned long long)read_id, kind, target, (long long)(offset + received), ask, length, chunk_index++);
104
+ if (n <= 0 || n >= sizeof(request) || write_exact(3, request, n) < 0) return -1;
105
+ int32_t actual;
106
+ if (read_exact(4, &actual, sizeof(actual)) < 0 || actual < 0) { errno = EACCES; return -1; }
107
+ if ((size_t)actual > ask || read_exact(4, (char *)buffer + received, actual) < 0) { errno = EIO; return -1; }
108
+ received += (size_t)actual;
109
+ if ((size_t)actual < ask) break;
110
+ }
111
+ return (ssize_t)received;
112
+ }
113
+ __attribute__((constructor)) static void handshake(void) {
114
+ const char *nonce = getenv("ENTROROVE_FIXED_GIT_NONCE");
115
+ char request[200]; int32_t accepted;
116
+ if (!nonce || !getenv("ENTROROVE_FIXED_GIT_ROOT") || strlen(nonce) != 64) _exit(199);
117
+ int n = snprintf(request, sizeof(request), "HELLO\tentrorove.fixed-git/1\t%s\n", nonce);
118
+ if (n <= 0 || n >= sizeof(request) || write_exact(3, request, n) < 0
119
+ || read_exact(4, &accepted, sizeof(accepted)) < 0 || accepted != 1) _exit(199);
120
+ }
121
+ static ssize_t controlled_read(int fd, void *buffer, size_t length) {
122
+ char target[PATH_MAX];
123
+ if (!tracked(fd, target)) return read(fd, buffer, length);
124
+ off_t offset = lseek(fd, 0, SEEK_CUR);
125
+ ssize_t actual = supply("read", target, buffer, length, offset);
126
+ if (actual >= 0 && lseek(fd, offset + actual, SEEK_SET) < 0) return -1;
127
+ return actual;
128
+ }
129
+ static ssize_t controlled_pread(int fd, void *buffer, size_t length, off_t offset) {
130
+ char target[PATH_MAX];
131
+ return tracked(fd, target) ? supply("pread", target, buffer, length, offset) : pread(fd, buffer, length, offset);
132
+ }
133
+ static void *controlled_mmap(void *address, size_t length, int protection, int flags, int fd, off_t offset) {
134
+ char target[PATH_MAX];
135
+ if (!tracked(fd, target)) return mmap(address, length, protection, flags, fd, offset);
136
+ if (protection != PROT_READ || flags != MAP_PRIVATE || address || !length || length > MAX_MAPPING) {
137
+ errno = EACCES; return MAP_FAILED;
138
+ }
139
+ void *buffer = malloc(length);
140
+ if (!buffer) return MAP_FAILED;
141
+ ssize_t actual = supply("mmap", target, buffer, length, offset);
142
+ if (actual < 0 || (size_t)actual != length) { free(buffer); errno = EACCES; return MAP_FAILED; }
143
+ for (size_t i = 0; i < 512; i++) if (!mappings[i].address) {
144
+ mappings[i] = (struct mapping){ buffer, length }; return buffer;
145
+ }
146
+ free(buffer); errno = ENOMEM; return MAP_FAILED;
147
+ }
148
+ static int controlled_munmap(void *address, size_t length) {
149
+ for (size_t i = 0; i < 512; i++) if (mappings[i].address == address) {
150
+ if (mappings[i].length != length) { errno = EINVAL; return -1; }
151
+ mappings[i].address = NULL; free(address); return 0;
152
+ }
153
+ return munmap(address, length);
154
+ }
155
+ static int link_target(const char *file, char *target) {
156
+ char absolute[PATH_MAX], parent[PATH_MAX];
157
+ int n;
158
+ if (file[0] == '/') n = snprintf(absolute, sizeof(absolute), "%s", file);
159
+ else {
160
+ if (!getcwd(parent, sizeof(parent))) return -1;
161
+ n = snprintf(absolute, sizeof(absolute), "%s/%s", parent, file);
162
+ }
163
+ if (n <= 0 || n >= sizeof(absolute)) return -1;
164
+ char *name = strrchr(absolute, '/');
165
+ if (!name) return -1;
166
+ *name = 0;
167
+ if (!realpath(absolute, parent)) return -1;
168
+ n = snprintf(target, PATH_MAX, "%s/%s", parent, name + 1);
169
+ return n > 0 && n < PATH_MAX ? 0 : -1;
170
+ }
171
+ static ssize_t controlled_readlink(const char *file, char *buffer, size_t length) {
172
+ char target[PATH_MAX];
173
+ if (link_target(file, target) < 0) { errno = EINVAL; return -1; }
174
+ return under_root(target) ? supply("readlink", target, buffer, length, 0) : readlink(file, buffer, length);
175
+ }
176
+ struct memory_file { FILE *file; void *buffer; int fd; };
177
+ static struct memory_file memory_files[128];
178
+ struct stream_cursor { char target[PATH_MAX]; off_t offset; size_t length; int fd; };
179
+ static ssize_t stream_read(void *cookie, char *buffer, size_t length) {
180
+ struct stream_cursor *cursor = cookie;
181
+ ssize_t actual = supply("stdio", cursor->target, buffer, length, cursor->offset);
182
+ if (actual >= 0) {
183
+ cursor->offset += actual;
184
+ if (lseek(cursor->fd, cursor->offset, SEEK_SET) < 0) return -1;
185
+ }
186
+ return actual;
187
+ }
188
+ static off_t stream_position(struct stream_cursor *cursor, off_t offset, int whence) {
189
+ off_t base;
190
+ if (whence == SEEK_SET) base = 0;
191
+ else if (whence == SEEK_CUR) base = cursor->offset;
192
+ else if (whence == SEEK_END) base = cursor->length;
193
+ else { errno = EINVAL; return -1; }
194
+ if (offset < -base || offset > MAX_MAPPING - base) { errno = EINVAL; return -1; }
195
+ off_t next = base + offset;
196
+ if (lseek(cursor->fd, next, SEEK_SET) < 0) return -1;
197
+ cursor->offset = next; return next;
198
+ }
199
+ #ifdef __linux__
200
+ static int stream_seek(void *cookie, off64_t *offset, int whence) {
201
+ off_t next = stream_position(cookie, *offset, whence);
202
+ if (next < 0) return -1;
203
+ *offset = next; return 0;
204
+ }
205
+ #else
206
+ static int stream_read_darwin(void *cookie, char *buffer, int length) { return stream_read(cookie, buffer, length); }
207
+ static fpos_t stream_seek_darwin(void *cookie, fpos_t offset, int whence) { return stream_position(cookie, offset, whence); }
208
+ #endif
209
+ static FILE *memory_stream(const char *target, size_t length, int fd) {
210
+ struct stream_cursor *cursor = malloc(sizeof(*cursor));
211
+ if (!cursor) return NULL;
212
+ cursor->offset = lseek(fd, 0, SEEK_CUR); cursor->length = length; cursor->fd = fd;
213
+ if (cursor->offset < 0 || strlen(target) >= PATH_MAX) { free(cursor); errno = EACCES; return NULL; }
214
+ strcpy(cursor->target, target);
215
+ #ifdef __linux__
216
+ cookie_io_functions_t functions = { stream_read, NULL, stream_seek, NULL };
217
+ FILE *memory = fopencookie(cursor, "r", functions);
218
+ #else
219
+ FILE *memory = funopen(cursor, stream_read_darwin, NULL, stream_seek_darwin, NULL);
220
+ #endif
221
+ if (!memory) { free(cursor); return NULL; }
222
+ for (size_t i = 0; i < 128; i++) if (!memory_files[i].file) {
223
+ memory_files[i] = (struct memory_file){ memory, cursor, fd }; return memory;
224
+ }
225
+ fclose(memory); free(cursor); errno = ENOMEM; return NULL;
226
+ }
227
+ static FILE *controlled_fdopen(int fd, const char *mode) {
228
+ char target[PATH_MAX];
229
+ if (!tracked(fd, target)) return fdopen(fd, mode);
230
+ struct stat stat;
231
+ if ((strcmp(mode, "r") && strcmp(mode, "rb")) || fstat(fd, &stat) < 0
232
+ || !S_ISREG(stat.st_mode) || stat.st_size < 0 || stat.st_size > MAX_MAPPING) {
233
+ errno = EACCES; return NULL;
234
+ }
235
+ return memory_stream(target, (size_t)stat.st_size, fd);
236
+ }
237
+ static FILE *controlled_fopen(const char *file, const char *mode) {
238
+ char target[PATH_MAX];
239
+ if (link_target(file, target) < 0 || !under_root(target)) return fopen(file, mode);
240
+ if (strcmp(mode, "r") && strcmp(mode, "rb")) { errno = EACCES; return NULL; }
241
+ FILE *original = fopen(file, mode);
242
+ if (!original) return NULL;
243
+ for (size_t i = 0; i < 128; i++) if (memory_files[i].file == original) return original;
244
+ struct stat stat;
245
+ if (fstat(fileno(original), &stat) < 0 || !S_ISREG(stat.st_mode) || stat.st_size < 0 || stat.st_size > MAX_MAPPING) {
246
+ fclose(original); errno = EACCES; return NULL;
247
+ }
248
+ int fd = dup(fileno(original));
249
+ fclose(original);
250
+ if (fd < 0) return NULL;
251
+ FILE *result = memory_stream(target, (size_t)stat.st_size, fd);
252
+ if (!result) close(fd);
253
+ return result;
254
+ }
255
+ static int controlled_fclose(FILE *file) {
256
+ for (size_t i = 0; i < 128; i++) if (memory_files[i].file == file) {
257
+ void *buffer = memory_files[i].buffer;
258
+ memory_files[i].file = NULL;
259
+ int fd = memory_files[i].fd;
260
+ int result = fclose(file); close(fd); free(buffer); return result;
261
+ }
262
+ return fclose(file);
263
+ }
264
+ static int controlled_fileno(FILE *file) {
265
+ for (size_t i = 0; i < 128; i++) if (memory_files[i].file == file) return memory_files[i].fd;
266
+ return fileno(file);
267
+ }
268
+ INTERPOSE(controlled_fileno, fileno)
269
+ INTERPOSE(controlled_fdopen, fdopen)
270
+ INTERPOSE(controlled_fopen, fopen)
271
+ INTERPOSE(controlled_fclose, fclose)
272
+ INTERPOSE(controlled_read, read)
273
+ INTERPOSE(controlled_pread, pread)
274
+ INTERPOSE(controlled_mmap, mmap)
275
+ INTERPOSE(controlled_munmap, munmap)
276
+ INTERPOSE(controlled_readlink, readlink)
277
+
278
+ #ifdef __linux__
279
+ #undef read
280
+ #undef pread
281
+ #undef readlink
282
+ #undef mmap
283
+ #undef munmap
284
+ #undef fopen
285
+ #undef fdopen
286
+ #undef fclose
287
+ #undef fileno
288
+ ssize_t read(int fd, void *buffer, size_t length) { return controlled_read(fd, buffer, length); }
289
+ ssize_t pread(int fd, void *buffer, size_t length, off_t offset) { return controlled_pread(fd, buffer, length, offset); }
290
+ ssize_t pread64(int fd, void *buffer, size_t length, off64_t offset) { return controlled_pread(fd, buffer, length, offset); }
291
+ void *mmap(void *address, size_t length, int protection, int flags, int fd, off_t offset) { return controlled_mmap(address, length, protection, flags, fd, offset); }
292
+ void *mmap64(void *address, size_t length, int protection, int flags, int fd, off64_t offset) { return controlled_mmap(address, length, protection, flags, fd, offset); }
293
+ int munmap(void *address, size_t length) { return controlled_munmap(address, length); }
294
+ ssize_t readlink(const char *file, char *buffer, size_t length) { return controlled_readlink(file, buffer, length); }
295
+ ssize_t readlinkat(int fd, const char *file, char *buffer, size_t length) {
296
+ if (file[0] == '/' || fd == AT_FDCWD) return controlled_readlink(file, buffer, length);
297
+ char directory[PATH_MAX], absolute[PATH_MAX];
298
+ if (!tracked(fd, directory)) return next_readlinkat(fd, file, buffer, length);
299
+ int n = snprintf(absolute, sizeof(absolute), "%s/%s", directory, file);
300
+ if (n < 0 || n >= sizeof(absolute)) { errno = EACCES; return -1; }
301
+ return controlled_readlink(absolute, buffer, length);
302
+ }
303
+ FILE *fopen(const char *file, const char *mode) { return controlled_fopen(file, mode); }
304
+ FILE *fopen64(const char *file, const char *mode) { return controlled_fopen(file, mode); }
305
+ FILE *fdopen(int fd, const char *mode) { return controlled_fdopen(fd, mode); }
306
+ int fclose(FILE *file) { return controlled_fclose(file); }
307
+ int fileno(FILE *file) { return controlled_fileno(file); }
308
+ int fileno_unlocked(FILE *file) { return controlled_fileno(file); }
309
+ ssize_t __read_chk(int fd, void *buffer, size_t length, size_t capacity) {
310
+ if (length > capacity) { errno = EACCES; return -1; }
311
+ return controlled_read(fd, buffer, length);
312
+ }
313
+ #endif
@@ -0,0 +1,109 @@
1
+ import { readFixedNativeAttributeChunk, createFixedNativeAttributeRequest } from './local-budget-git-native-attributes.mjs';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { randomBytes, createHash } from 'node:crypto';
6
+ import { fail } from './local-budget-grants.mjs';
7
+ import { gitReadDeadline } from './local-budget-git.mjs';
8
+ import { fixedScratchArguments, assertGitScratchSession, scratchNumstat } from './local-budget-git-scratch-loader.mjs';
9
+ import { fixedGitNativeRuntime, assertFixedGitRuntime, fixedGitEnvironment } from './local-budget-git-native-runtime.mjs';
10
+ import { readFixedNativeGitChunk, createFixedNativeGitRequest, assertFixedNativeGitSources } from './local-budget-git-native-reads.mjs';
11
+
12
+ const invalid = () => fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git content reader handshake or bounded source protocol is invalid');
13
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
14
+ async function canonicalTarget(target) { return path.join(await fs.realpath(path.dirname(target)), path.basename(target)); }
15
+ function request(line, descriptors, progress) {
16
+ const parts = line.split('\t');
17
+ if (parts.length !== 8 || parts[0] !== 'R' || !['read', 'pread', 'mmap', 'readlink', 'stdio'].includes(parts[2])) invalid();
18
+ const [id, offset, count, length, index] = [1, 4, 5, 6, 7].map(i => {
19
+ if (!/^(?:0|[1-9][0-9]{0,15})$/.test(parts[i])) invalid();
20
+ const value = Number(parts[i]); if (!Number.isSafeInteger(value)) invalid(); return value;
21
+ });
22
+ const descriptor = descriptors.get(parts[3]);
23
+ if (!descriptor || id < 1 || count < 1 || count > 65536 || length < count || length > 2147483647
24
+ || (parts[2] === 'readlink' && offset) || (parts[2] === 'mmap' && offset + count > descriptor.bytes)) invalid();
25
+ let prior = progress.get(id);
26
+ if (!prior) {
27
+ if (id !== progress.size + 1 || index !== 0) invalid();
28
+ if (parts[2] === 'mmap' && offset + length > descriptor.bytes) invalid();
29
+ prior = { bytes: 0, started: false, offset, length, index: 0, path: parts[3], kind: parts[2] }; progress.set(id, prior);
30
+ }
31
+ if (prior.index !== index || prior.path !== parts[3] || prior.kind !== parts[2] || prior.length !== length
32
+ || offset !== prior.offset + prior.bytes || count !== Math.min(65536, length - prior.bytes)) invalid();
33
+ return { descriptor, prior, parsed: { id, kind: parts[2], offset, count, length, index } };
34
+ }
35
+ async function respond(pipe, count, bytes) {
36
+ const header = Buffer.alloc(4); header.writeInt32LE(count);
37
+ await new Promise((resolve, reject) => pipe.write(bytes ? Buffer.concat([header, bytes]) : header, error => error ? reject(error) : resolve()));
38
+ }
39
+ async function controlBytes(session, target, parsed) {
40
+ if (target !== await canonicalTarget(session.attributes)) invalid();
41
+ const stat = await fs.lstat(target);
42
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || stat.mode & 0o077) invalid();
43
+ const bytes = await fs.readFile(target);
44
+ if (hash(bytes) !== hash(Buffer.from(session.options.attributes))) invalid();
45
+ return bytes.subarray(parsed.offset, parsed.offset + parsed.count);
46
+ }
47
+ export async function fixedNativeScratchComparison(input, session) {
48
+ assertGitScratchSession(session);
49
+ await assertFixedNativeGitSources(input, session);
50
+ const runtime = await fixedGitNativeRuntime(input), directory = await fs.realpath(session.directory);
51
+ const descriptors = new Map();
52
+ for (const descriptor of session.descriptors) descriptors.set(await canonicalTarget(descriptor.target), descriptor);
53
+ // Derived attributes retain exact original source segments; only fixed conversion flags carry no source bytes.
54
+ const control = await canonicalTarget(session.attributes);
55
+ descriptors.set(control, { path: null, bytes: Buffer.byteLength(session.options.attributes), control: true });
56
+ const state = await assertFixedNativeGitSources(input, session), deadline = gitReadDeadline(state);
57
+ const nonce = randomBytes(32).toString('hex');
58
+ const child = spawn(runtime.git, fixedScratchArguments(session), { cwd: directory,
59
+ env: { ...fixedGitEnvironment(), GIT_CEILING_DIRECTORIES: path.dirname(directory), GIT_DISCOVERY_ACROSS_FILESYSTEM: '0', ...runtime.loaderEnv,
60
+ ENTROROVE_FIXED_GIT_ROOT: directory, ENTROROVE_FIXED_GIT_NONCE: nonce }, stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] });
61
+ let ready = false, buffer = '', output = '', failed = null, finished = false, queue = Promise.resolve();
62
+ const progress = new Map(); child.stderr.resume(); child.stdio[4].on('error', error => { failed ??= error; });
63
+ child.stdout.on('data', bytes => { output += bytes.toString('utf8'); if (Buffer.byteLength(output) > 2097152) { failed ??= Object.assign(new Error('fixed Git result exceeded its limit'), { code: 'ENTROROVE_GIT_FAILED' }); child.kill('SIGKILL'); } });
64
+ const handle = async line => {
65
+ if (line === `HELLO\tentrorove.fixed-git/1\t${nonce}`) {
66
+ if (ready) invalid(); ready = true; await assertFixedGitRuntime(runtime); await assertFixedNativeGitSources(input, session);
67
+ return respond(child.stdio[4], 1);
68
+ }
69
+ if (!ready || failed) invalid();
70
+ const { descriptor, prior, parsed } = request(line, descriptors, progress);
71
+ let actual;
72
+ if (descriptor.control && session.attributeDescriptors.length) {
73
+ const result = await readFixedNativeAttributeChunk(input, session, await createFixedNativeAttributeRequest(input, session, parsed));
74
+ actual = result.actual; prior.derived = result.derived;
75
+ } else actual = descriptor.control ? await controlBytes(session, control, parsed)
76
+ : await readFixedNativeGitChunk(input, descriptor, await createFixedNativeGitRequest(input, session, descriptor, parsed));
77
+ prior.bytes += actual.length; prior.started = true; prior.index++;
78
+ await assertFixedNativeGitSources(input, session); await respond(child.stdio[4], actual.length, actual);
79
+ };
80
+ child.stdio[3].on('data', bytes => {
81
+ buffer += bytes.toString('utf8');
82
+ if (Buffer.byteLength(buffer) > 8192) { failed ??= Object.assign(new Error('fixed Git source protocol exceeded its limit'), { code: 'ENTROROVE_GIT_READER_UNSUPPORTED' }); child.kill('SIGKILL'); return; }
83
+ let end;
84
+ while ((end = buffer.indexOf('\n')) >= 0) {
85
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
86
+ queue = queue.then(() => handle(line)).catch(async error => {
87
+ failed ??= error;
88
+ await respond(child.stdio[4], -1).catch(() => {}); child.kill('SIGKILL');
89
+ });
90
+ }
91
+ });
92
+ const done = new Promise(resolve => {
93
+ child.once('error', error => { failed ??= error; finished = true; resolve(null); });
94
+ child.once('close', code => { finished = true; resolve(code); });
95
+ });
96
+ try {
97
+ while (!finished) {
98
+ await Promise.race([done, new Promise(resolve => setTimeout(resolve, 25))]);
99
+ if (Date.now() >= deadline) fail('ENTROROVE_BUDGET_LEASE_EXPIRED', 'fixed Git comparison authorization expired');
100
+ await assertFixedNativeGitSources(input, session); await assertFixedGitRuntime(runtime);
101
+ }
102
+ const code = await done; await queue;
103
+ if (failed) throw failed;
104
+ if (!ready) invalid();
105
+ if (![0, 1].includes(code) || buffer) fail('ENTROROVE_GIT_FAILED', 'fixed Git sealed comparison failed');
106
+ await assertFixedNativeGitSources(input, session); await assertFixedGitRuntime(runtime);
107
+ return scratchNumstat(output);
108
+ } finally { child.kill('SIGKILL'); await done; await queue; }
109
+ }
@@ -0,0 +1,33 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { fail } from './local-budget-grants.mjs';
3
+ import { observeGitFacts } from './local-scope-repository.mjs';
4
+ import { gitReadBinding } from './local-budget-git.mjs';
5
+ import { budgetLocation, loadBudget } from './local-budget-store.mjs';
6
+ const outcomes = new WeakMap();
7
+ const completedDenials = new Set(['ENTROROVE_BUDGET_EXHAUSTED', 'ENTROROVE_BUDGET_LEASE_EXPIRED',
8
+ 'ENTROROVE_BUDGET_SCOPE', 'ENTROROVE_BUDGET_HANDOFF_REVOKED', 'ENTROROVE_BUDGET_COMPLETED']);
9
+ export function getBudgetGitReadOutcome(value) {
10
+ return value !== null && (typeof value === 'object' || typeof value === 'function') ? outcomes.get(value) ?? null : null;
11
+ }
12
+ async function record(value, binding, initial) {
13
+ const { state } = await loadBudget(await budgetLocation(binding.input));
14
+ outcomes.set(value, Object.freeze({ flowId: binding.input.flowId, repositoryRoot: binding.root, budgetHome: binding.input.home,
15
+ initialSequence: initial.ledger.sequence, initialHead: initial.ledger.head, sequence: state.ledger.sequence, head: state.ledger.head,
16
+ readCalls: state.readCalls, tokenEstimate: state.tokenEstimate, files: Object.freeze([...state.files]) }));
17
+ }
18
+ /** Fixed scope Git observer only; no callback, command or externally writable receipt store. */
19
+ export async function observeBudgetedGitFacts(root, files, baselineRef, input = { repositoryRoot: root }) {
20
+ const binding = await gitReadBinding(input);
21
+ if (await fs.realpath(root) !== binding.root) fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git observation cannot change its original repository root');
22
+ if (!binding.input.flowId) return observeGitFacts(root, files, baselineRef, input);
23
+ const { state: initial } = await loadBudget(await budgetLocation(binding.input));
24
+ try {
25
+ const result = await observeGitFacts(root, files, baselineRef, binding.input);
26
+ await record(result, binding, initial); return result;
27
+ } catch (error) {
28
+ // These fixed-reader denials complete only after every received source byte was committed.
29
+ // Unknown filesystem/journal failures retain pending accounting without a completion receipt.
30
+ if (completedDenials.has(error.code)) await record(error, binding, initial);
31
+ throw error;
32
+ }
33
+ }
@@ -0,0 +1,158 @@
1
+ import { boundGitAttributeSegments, hasSealedBudgetGitAttributes } from './local-budget-git-attributes.mjs';
2
+ import { constants, promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import { createHash } from 'node:crypto';
7
+ import { fail, relativePath } from './local-budget-grants.mjs';
8
+ import { verifyFixedGitBlob, verifyGitBlobBytes, gitReadExecution, gitSourceDescriptor } from './local-budget-git-loader.mjs';
9
+ import { gitReadBinding } from './local-budget-git.mjs';
10
+ import { readBudgetedProjectFile, copyBudgetedProjectBuffer, verifyBudgetedProjectBuffer } from './local-budget-reads.mjs';
11
+
12
+ const sessions = new WeakMap(), descriptorBindings = new WeakMap(), descriptorContents = new WeakMap();
13
+ export function assertGitScratchSession(session) {
14
+ if (!sessions.has(session)) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git requires an adapter-created sealed source session');
15
+ }
16
+ async function assertOriginalInput(input, original) {
17
+ if (!input || input.flowId !== original.flowId || await fs.realpath(input.repositoryRoot) !== original.repositoryRoot
18
+ || original.home && (!input.home || await fs.realpath(input.home) !== original.home)) {
19
+ fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git source cannot change its original home, root or flow');
20
+ }
21
+ const actual = (await gitReadBinding(input)).input;
22
+ if (['home', 'repositoryRoot', 'flowId'].some(key => actual[key] !== original[key])) {
23
+ fail('ENTROROVE_GIT_SOURCE_INVALID', 'fixed Git source requires its original home, root and flow');
24
+ }
25
+ }
26
+ export async function assertGitScratchSessionInput(input, session) {
27
+ assertGitScratchSession(session); await assertOriginalInput(input, sessions.get(session));
28
+ }
29
+ export async function assertGitScratchInput(input, descriptor) {
30
+ if (!descriptorBindings.has(descriptor)) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git requires an adapter-created source descriptor');
31
+ await assertOriginalInput(input, descriptorBindings.get(descriptor)); await assertGitScratch(descriptor);
32
+ }
33
+ function sealedDescriptor(input, value, content) {
34
+ Object.freeze(value.stat); const descriptor = Object.freeze(value); descriptorBindings.set(descriptor, input);
35
+ if (content) descriptorContents.set(descriptor, Buffer.from(content));
36
+ return descriptor;
37
+ }
38
+ const invalid = message => fail('ENTROROVE_GIT_SOURCE_INVALID', message);
39
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
40
+ function comparisonOptions(value) {
41
+ if (!value || typeof value !== 'object' || Array.isArray(value)
42
+ || Object.keys(value).some(key => !['attributes', 'attributeBinding', 'binaryDrivers', 'renames', 'algorithm', 'indentHeuristic', 'autocrlf', 'eol', 'renameLimit'].includes(key))
43
+ || typeof value.attributes !== 'string' || /(?:^|[ \t])filter=/.test(value.attributes) || value.algorithm !== undefined && !['myers', 'minimal', 'patience', 'histogram'].includes(value.algorithm)
44
+ || value.renames !== undefined && ![false, true, 'copies'].includes(value.renames)
45
+ || value.autocrlf !== undefined && !['true', 'false', 'input'].includes(value.autocrlf)
46
+ || value.eol !== undefined && !['lf', 'crlf', 'native'].includes(value.eol)
47
+ || value.renameLimit !== undefined && (!Number.isSafeInteger(value.renameLimit) || value.renameLimit < 0)
48
+ || value.indentHeuristic !== undefined && typeof value.indentHeuristic !== 'boolean'
49
+ || value.binaryDrivers !== undefined && (!Array.isArray(value.binaryDrivers) || value.binaryDrivers.some(driver => !driver
50
+ || Object.keys(driver).sort().join(',') !== 'binary,name' || !/^[A-Za-z0-9_.-]+$/.test(driver.name) || typeof driver.binary !== 'boolean'))) {
51
+ fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'Git comparison accepts only fixed captured diff metadata, never commands or callbacks');
52
+ }
53
+ const { attributeBinding, ...fields } = value;
54
+ const options = { ...structuredClone(fields), ...(attributeBinding ? { attributeBinding } : {}) };
55
+ if (options.binaryDrivers) { options.binaryDrivers.forEach(Object.freeze); Object.freeze(options.binaryDrivers); }
56
+ return Object.freeze(options);
57
+ }
58
+ async function boundEntry(input, entry) {
59
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Buffer.isBuffer(entry.content)
60
+ || !['100644', '100755', '120000'].includes(entry.mode)) invalid('Git scratch needs actual regular-file or symlink source bytes');
61
+ const relative = relativePath(entry.path), descriptor = { path: relative, bytes: entry.content.length,
62
+ objectId: entry.objectId ?? null, commitId: entry.commitId ?? null, mode: entry.mode, role: 'git-blob' };
63
+ if (descriptor.objectId !== null || descriptor.commitId !== null) {
64
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(descriptor.objectId ?? '')
65
+ || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(descriptor.commitId ?? '')) invalid('Historical scratch requires its actual commit and blob identity');
66
+ gitSourceDescriptor(descriptor); verifyGitBlobBytes(descriptor, entry.content); await verifyFixedGitBlob(input.repositoryRoot, descriptor, Date.now() + 10000);
67
+ } else if (!await verifyBudgetedProjectBuffer(input, path.join(input.repositoryRoot, relative), entry.content)) {
68
+ const content = await readBudgetedProjectFile(input, path.join(input.repositoryRoot, relative), { sourcePath: relative });
69
+ if (!content.equals(entry.content)) invalid('Working scratch bytes differ from the original bound source');
70
+ }
71
+ return { ...descriptor, role: 'git-comparison', sha256: hash(entry.content) };
72
+ }
73
+ async function writeSide(input, directory, side, entries) {
74
+ if (!Array.isArray(entries)) invalid('Git scratch sides require arrays of original source entries');
75
+ const base = path.join(directory, side); await fs.mkdir(base, { mode: 0o700 }); const descriptors = [], seen = new Set();
76
+ for (const entry of entries) {
77
+ const descriptor = await boundEntry(input, entry);
78
+ if ([...seen].some(value => value === descriptor.path || value.startsWith(`${descriptor.path}/`) || descriptor.path.startsWith(`${value}/`))) {
79
+ invalid('Git scratch source paths repeat or overlap a file or symlink parent');
80
+ }
81
+ seen.add(descriptor.path);
82
+ const target = path.join(base, descriptor.path); await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
83
+ if (entry.mode === '120000') {
84
+ const value = entry.content.toString('utf8');
85
+ if (!value || value.includes('\0') || !Buffer.from(value).equals(entry.content)) invalid('Git scratch cannot safely represent symlink bytes');
86
+ await fs.symlink(value, target);
87
+ } else await fs.writeFile(target, entry.content, { mode: entry.mode === '100755' ? 0o700 : 0o600, flag: 'wx' });
88
+ descriptors.push(sealedDescriptor(input, { ...descriptor, target, stat: await fs.lstat(target, { bigint: true }) }, entry.content));
89
+ }
90
+ return descriptors;
91
+ }
92
+ export async function createGitScratch(input, before, after, value) {
93
+ input = (await gitReadBinding(input)).input;
94
+ const copy = entries => {
95
+ if (!Array.isArray(entries)) invalid('Git scratch sides require arrays');
96
+ return entries.map(entry => {
97
+ if (!entry || !Buffer.isBuffer(entry.content)) invalid('Git scratch requires actual source buffers');
98
+ return { ...entry, content: copyBudgetedProjectBuffer(entry.content) };
99
+ });
100
+ };
101
+ before = copy(before); after = copy(after);
102
+ const options = comparisonOptions(value), attributeSegments = await boundGitAttributeSegments(input, options), directory = await fs.mkdtemp(path.join(os.tmpdir(), 'entrorove-budget-git-'));
103
+ try {
104
+ await fs.chmod(directory, 0o700);
105
+ const descriptors = [...await writeSide(input, directory, 'before', before), ...await writeSide(input, directory, 'after', after)];
106
+ const attributes = path.join(directory, 'attributes'); await fs.writeFile(attributes, options.attributes, { mode: 0o600, flag: 'wx' });
107
+ const attributeStat = Object.freeze(await fs.lstat(attributes, { bigint: true }));
108
+ const attributeDescriptors = attributeSegments.map(segment => sealedDescriptor(input, { ...segment, target: attributes, stat: attributeStat, bytes: Buffer.byteLength(options.attributes) }));
109
+ const session = Object.freeze({ directory, attributes, attributeStat, attributeDescriptors, options, descriptors,
110
+ capturedAttributes: hasSealedBudgetGitAttributes(options) }); Object.freeze(descriptors); Object.freeze(attributeDescriptors); sessions.set(session, input); return session;
111
+ } catch (error) { await fs.rm(directory, { recursive: true, force: true }); throw error; }
112
+ }
113
+ export async function assertGitScratch(descriptor) {
114
+ if (!descriptorBindings.has(descriptor)) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'fixed Git source descriptor is not an opaque adapter identity');
115
+ const stat = await fs.lstat(descriptor.target, { bigint: true }), original = descriptor.stat;
116
+ if (stat.dev !== original.dev || stat.ino !== original.ino || stat.size !== original.size || stat.mode !== original.mode
117
+ || stat.mtimeNs !== original.mtimeNs || stat.ctimeNs !== original.ctimeNs) invalid('Private Git scratch changed during its fixed comparison');
118
+ }
119
+ export function fixedScratchArguments(session) {
120
+ assertGitScratchSession(session);
121
+ const args = ['--no-optional-locks', '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', '-c', 'diff.external=', '-c', `core.attributesFile=${session.attributes}`];
122
+ for (const driver of session.options.binaryDrivers ?? []) args.push('-c', `diff.${driver.name}.binary=${driver.binary}`);
123
+ if (session.options.autocrlf !== undefined) args.push('-c', `core.autocrlf=${session.options.autocrlf}`);
124
+ if (session.options.eol !== undefined) args.push('-c', `core.eol=${session.options.eol}`);
125
+ args.push('diff', '--no-index', '--no-ext-diff', '--no-textconv', '--numstat', '-z',
126
+ session.options.renames === false ? '--no-renames' : session.options.renames === 'copies' ? '-C' : '-M');
127
+ if (session.options.algorithm) args.push(`--diff-algorithm=${session.options.algorithm}`);
128
+ if (session.options.indentHeuristic === false) args.push('--no-indent-heuristic');
129
+ if (session.options.renameLimit !== undefined) args.push(`-l${session.options.renameLimit}`);
130
+ args.push('--', 'before', 'after');
131
+ return args;
132
+ }
133
+ export function fixedScratchComparison(session, deadlineAt) {
134
+ assertGitScratchSession(session);
135
+ if (sessions.get(session).flowId) fail('ENTROROVE_GIT_READER_UNSUPPORTED', 'a budget-bound Git session requires the fixed metered native process');
136
+ const child = spawn('/usr/bin/git', fixedScratchArguments(session), { ...gitReadExecution(session.directory, deadlineAt), stdio: ['ignore', 'pipe', 'pipe'] });
137
+ child.stderr.resume(); let output = '', oversized = false;
138
+ child.stdout.on('data', chunk => { output += chunk.toString('utf8'); if (output.length > 2097152) { oversized = true; child.kill('SIGKILL'); } });
139
+ const done = new Promise(resolve => { child.once('error', () => resolve({ ok: false })); child.once('close', code => resolve({ ok: !oversized && [0, 1].includes(code), output })); });
140
+ return { done, close: () => child.kill('SIGKILL') };
141
+ }
142
+ export function scratchNumstat(output) {
143
+ let lines = 0, binary = false, present = false;
144
+ for (const entry of output.split('\0')) { const match = /^(\d+|-)\t(\d+|-)\t/.exec(entry); if (!match) continue;
145
+ present = true; if (match[1] === '-' || match[2] === '-') binary = true; else lines += Number(match[1]) + Number(match[2]); }
146
+ return { lines, binary, present };
147
+ }
148
+ export function verifyScratchBytes(descriptor, content) {
149
+ if (content.length !== descriptor.bytes || hash(content) !== descriptor.sha256) invalid('Actual private scratch bytes differ from their original source binding');
150
+ }
151
+
152
+ export function verifyNativeScratchChunk(descriptor, offset, content) {
153
+ const original = descriptorContents.get(descriptor);
154
+ if (!original || !Number.isSafeInteger(offset) || offset < 0 || !Buffer.isBuffer(content)
155
+ || !content.equals(original.subarray(offset, offset + content.length))) {
156
+ invalid('Actual native scratch bytes differ from their opaque original source binding');
157
+ }
158
+ }