@entrorove/skill 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGREEMENT.md +6 -6
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +18 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +35 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +24 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +65 -24
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +126 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-error.mjs +23 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +106 -0
- package/local-formula-engine.mjs +125 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +142 -0
- package/local-ledger.mjs +15 -9
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +103 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +89 -0
- package/local-mutation-files.mjs +270 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +41 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +164 -3
- package/release-state.json +164 -18
- package/release.mjs +242 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
|
@@ -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
|
+
}
|