jev-agent-tools 0.2.0 → 0.3.0
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/CHANGELOG.md +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
package/dist/adapters/git.js
CHANGED
|
@@ -2,6 +2,7 @@ import { lstat, readlink } from "node:fs/promises";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { GIT_BLOB_BATCH_SIZE, STATE_MAX_CHARS, TIMEOUT_MS, } from "../constants.js";
|
|
4
4
|
import { parseDiff } from "../core/diff.js";
|
|
5
|
+
import { isSecretPath } from "../core/secret-path.js";
|
|
5
6
|
import { buildUnits, } from "../core/units.js";
|
|
6
7
|
import { admitGitPath, admitRepoMetadata, openRepoFile } from "./files.js";
|
|
7
8
|
import { shareGitInventory } from "./git-inventory.js";
|
|
@@ -36,14 +37,23 @@ async function git(exec, options, args) {
|
|
|
36
37
|
if (result.killed)
|
|
37
38
|
return {
|
|
38
39
|
ok: false,
|
|
40
|
+
cause: options.signal?.aborted ? "cancelled" : "git_failure",
|
|
39
41
|
error: "Git interrupted (cancelled or timed out).",
|
|
40
42
|
};
|
|
41
43
|
if (result.code !== 0)
|
|
42
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
cause: "git_failure",
|
|
47
|
+
error: `Git failed: ${result.stderr}`,
|
|
48
|
+
};
|
|
43
49
|
return { ok: true, text: result.stdout };
|
|
44
50
|
}
|
|
45
51
|
catch (error) {
|
|
46
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
cause: options.signal?.aborted ? "cancelled" : "git_failure",
|
|
55
|
+
error: `Unable to execute git: ${String(error)}`,
|
|
56
|
+
};
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
async function readWorktree(options, exec, path, inventory) {
|
|
@@ -186,12 +196,21 @@ export async function collectDiff(exec, options) {
|
|
|
186
196
|
catch {
|
|
187
197
|
return {
|
|
188
198
|
ok: false,
|
|
199
|
+
cause: "git_failure",
|
|
189
200
|
error: "Inconsistent Git snapshots; collect again.",
|
|
190
201
|
kind: "inconsistent_diff",
|
|
191
202
|
};
|
|
192
203
|
}
|
|
193
204
|
const unsupported = [];
|
|
205
|
+
// Secret-named paths (either side of a rename) stay named exclusions;
|
|
206
|
+
// excluding them from every later pathspec keeps Git from emitting their
|
|
207
|
+
// patch text or pairing them into a rename.
|
|
208
|
+
const secret = candidates
|
|
209
|
+
.filter((file) => isSecretPath(file.path) || isSecretPath(file.oldPath))
|
|
210
|
+
.map((file) => file.path);
|
|
194
211
|
await Promise.all(candidates.map(async (file) => {
|
|
212
|
+
if (secret.includes(file.path))
|
|
213
|
+
return;
|
|
195
214
|
try {
|
|
196
215
|
const stat = await lstat(resolve(cwd, file.path));
|
|
197
216
|
if (!stat.isFile() &&
|
|
@@ -204,8 +223,11 @@ export async function collectDiff(exec, options) {
|
|
|
204
223
|
/* Missing tracked paths remain deletions. */
|
|
205
224
|
}
|
|
206
225
|
}));
|
|
207
|
-
for (const path of unsupported)
|
|
226
|
+
for (const path of [...unsupported, ...secret])
|
|
208
227
|
paths.push(`:(top,exclude,literal)${path}`);
|
|
228
|
+
for (const file of candidates)
|
|
229
|
+
if (secret.includes(file.path) && file.oldPath !== file.path)
|
|
230
|
+
paths.push(`:(top,exclude,literal)${file.oldPath}`);
|
|
209
231
|
const prefix = [
|
|
210
232
|
"diff",
|
|
211
233
|
"--no-color",
|
|
@@ -280,7 +302,11 @@ export async function collectDiff(exec, options) {
|
|
|
280
302
|
!untracked.ok ||
|
|
281
303
|
!current.ok ||
|
|
282
304
|
!base.ok)
|
|
283
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
cause: "git_failure",
|
|
308
|
+
error: "Incomplete Git collection.",
|
|
309
|
+
};
|
|
284
310
|
const currentInventory = new Set(current.text.split("\0").filter(Boolean));
|
|
285
311
|
const baseInventory = new Map();
|
|
286
312
|
for (const entry of base.text.split("\0")) {
|
|
@@ -301,16 +327,18 @@ export async function collectDiff(exec, options) {
|
|
|
301
327
|
catch {
|
|
302
328
|
return {
|
|
303
329
|
ok: false,
|
|
330
|
+
cause: "git_failure",
|
|
304
331
|
error: "Inconsistent Git snapshots; collect again.",
|
|
305
332
|
kind: "inconsistent_diff",
|
|
306
333
|
};
|
|
307
334
|
}
|
|
308
|
-
files.push(...candidates.filter((file) => unsupported.includes(file.path)));
|
|
335
|
+
files.push(...candidates.filter((file) => unsupported.includes(file.path) || secret.includes(file.path)));
|
|
309
336
|
const objects = new Map();
|
|
310
337
|
for (const file of files) {
|
|
311
338
|
if (/^[A?]/.test(file.status) ||
|
|
312
339
|
file.binary ||
|
|
313
|
-
unsupported.includes(file.path)
|
|
340
|
+
unsupported.includes(file.path) ||
|
|
341
|
+
secret.includes(file.path))
|
|
314
342
|
continue;
|
|
315
343
|
const admission = await admitGitPath(cwd, file.oldPath, options.base, exec, options.signal, baseInventory);
|
|
316
344
|
if (!admission.ok)
|
|
@@ -328,6 +356,18 @@ export async function collectDiff(exec, options) {
|
|
|
328
356
|
if (!loaded.ok)
|
|
329
357
|
return loaded;
|
|
330
358
|
const collectFile = async (file) => {
|
|
359
|
+
if (secret.includes(file.path))
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
file: {
|
|
363
|
+
...file,
|
|
364
|
+
hunks: [],
|
|
365
|
+
binary: false,
|
|
366
|
+
before: null,
|
|
367
|
+
after: null,
|
|
368
|
+
limitation: "secret_pattern",
|
|
369
|
+
},
|
|
370
|
+
};
|
|
331
371
|
const historical = !/^[A?]/.test(file.status);
|
|
332
372
|
const admission = historical
|
|
333
373
|
? await admitGitPath(cwd, file.oldPath, options.base, exec, options.signal, baseInventory)
|
|
@@ -382,7 +422,11 @@ export async function collectDiff(exec, options) {
|
|
|
382
422
|
if (!/^[A?]/.test(file.status)) {
|
|
383
423
|
const object = objects.get(file.oldPath);
|
|
384
424
|
if (!object)
|
|
385
|
-
return {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
cause: "git_failure",
|
|
428
|
+
error: "Incomplete Git base tree; collect again.",
|
|
429
|
+
};
|
|
386
430
|
if (object.size > STATE_MAX_CHARS)
|
|
387
431
|
limitation = "too_large";
|
|
388
432
|
else if (loaded.invalid.has(object.id))
|
|
@@ -7,6 +7,7 @@ export async function readLocateFile(cwd, path, signal, exec) {
|
|
|
7
7
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
8
8
|
return {
|
|
9
9
|
ok: false,
|
|
10
|
+
cause: "forbidden_path",
|
|
10
11
|
error: `Internal URLs are not files; use read for ${path}.`,
|
|
11
12
|
};
|
|
12
13
|
const opened = await openRepoFile(cwd, path, { exec, signal });
|
|
@@ -16,7 +17,11 @@ export async function readLocateFile(cwd, path, signal, exec) {
|
|
|
16
17
|
try {
|
|
17
18
|
const stat = await handle.stat();
|
|
18
19
|
if (!stat.isFile())
|
|
19
|
-
return {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
cause: "file_unavailable",
|
|
23
|
+
error: `Not a file: ${path}.`,
|
|
24
|
+
};
|
|
20
25
|
const buffer = Buffer.alloc(Math.min(stat.size + 1, LOCATE_WHOLE_MAX_BYTES + 1));
|
|
21
26
|
let count = 0;
|
|
22
27
|
while (count < buffer.length) {
|
|
@@ -27,11 +32,19 @@ export async function readLocateFile(cwd, path, signal, exec) {
|
|
|
27
32
|
count += read.bytesRead;
|
|
28
33
|
}
|
|
29
34
|
if (buffer.subarray(0, count).includes(0))
|
|
30
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
cause: "binary_or_non_utf8",
|
|
38
|
+
error: `Not a text file: ${path}.`,
|
|
39
|
+
};
|
|
31
40
|
if (count < buffer.length) {
|
|
32
41
|
const decoded = decodeUtf8(buffer.subarray(0, count));
|
|
33
42
|
if (!decoded.ok)
|
|
34
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
cause: "binary_or_non_utf8",
|
|
46
|
+
error: `${path}: ${decoded.error}`,
|
|
47
|
+
};
|
|
35
48
|
const text = decoded.text;
|
|
36
49
|
if (text.length <= LOCATE_WHOLE_MAX_CHARS)
|
|
37
50
|
return {
|
|
@@ -56,7 +69,11 @@ export async function readLocateFile(cwd, path, signal, exec) {
|
|
|
56
69
|
: scan;
|
|
57
70
|
}
|
|
58
71
|
catch (error) {
|
|
59
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
75
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
76
|
+
};
|
|
60
77
|
}
|
|
61
78
|
finally {
|
|
62
79
|
await handle.close();
|
|
@@ -127,14 +144,23 @@ export async function scanRange(cwd, path, range, signal, exec) {
|
|
|
127
144
|
break;
|
|
128
145
|
}
|
|
129
146
|
if (buffer.subarray(0, read.bytesRead).includes(0))
|
|
130
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
cause: "binary_or_non_utf8",
|
|
150
|
+
error: `Not a text file: ${path}.`,
|
|
151
|
+
};
|
|
131
152
|
bytes += read.bytesRead;
|
|
132
153
|
const decoded = decode(buffer.subarray(0, read.bytesRead));
|
|
133
154
|
if (!decoded.ok)
|
|
134
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
cause: "binary_or_non_utf8",
|
|
158
|
+
error: `${path}: ${decoded.error}`,
|
|
159
|
+
};
|
|
135
160
|
if (!consume(decoded.text))
|
|
136
161
|
return {
|
|
137
162
|
ok: false,
|
|
163
|
+
cause: "evidence_too_large",
|
|
138
164
|
error: `Selected range exceeds ${STATE_MAX_CHARS} chars; read a narrower range directly.`,
|
|
139
165
|
};
|
|
140
166
|
if (range && line > range.end)
|
|
@@ -142,16 +168,28 @@ export async function scanRange(cwd, path, range, signal, exec) {
|
|
|
142
168
|
}
|
|
143
169
|
const end = reachedEof ? decode() : { ok: true, text: "" };
|
|
144
170
|
if (!end.ok)
|
|
145
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
cause: "binary_or_non_utf8",
|
|
174
|
+
error: `${path}: ${end.error}`,
|
|
175
|
+
};
|
|
146
176
|
if (!consume(end.text))
|
|
147
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
cause: "evidence_too_large",
|
|
180
|
+
error: "Selected range too large.",
|
|
181
|
+
};
|
|
148
182
|
const lines = line - Number(!pending);
|
|
149
183
|
if (lines > 0 && (windows.at(-1)?.end ?? 0) < lines)
|
|
150
184
|
finishWindow(lines);
|
|
151
185
|
return { ok: true, bytes, lines, windows, text };
|
|
152
186
|
}
|
|
153
187
|
catch (error) {
|
|
154
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
191
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
192
|
+
};
|
|
155
193
|
}
|
|
156
194
|
finally {
|
|
157
195
|
await handle.close();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { PRIVATE_STORAGE_TIMEOUT_MS } from "../constants.js";
|
|
4
|
+
import { withoutApiKey } from "./shell.js";
|
|
4
5
|
const SYSTEM = "S-1-5-18";
|
|
5
6
|
const ADMINISTRATORS = "S-1-5-32-544";
|
|
6
7
|
const CREATOR_OWNER = "S-1-3-0";
|
|
@@ -49,16 +50,23 @@ foreach ($id in @($me.Value, '${SYSTEM}', '${ADMINISTRATORS}')) {
|
|
|
49
50
|
$acl.AddAccessRule($rule)
|
|
50
51
|
}
|
|
51
52
|
Set-Acl -LiteralPath $path -AclObject $acl`;
|
|
53
|
+
/**
|
|
54
|
+
* Host environment for the ACL helper, without the Jev API key. PowerShell 7
|
|
55
|
+
* parents export PSModulePath, which stops Windows PowerShell 5.1 from loading
|
|
56
|
+
* its own Get-Acl module; 5.1 rebuilds it when unset.
|
|
57
|
+
*/
|
|
58
|
+
export function powershellEnv(host, env) {
|
|
59
|
+
const childEnv = withoutApiKey({ ...host, ...env });
|
|
60
|
+
for (const key of Object.keys(childEnv))
|
|
61
|
+
if (key.toLowerCase() === "psmodulepath")
|
|
62
|
+
delete childEnv[key];
|
|
63
|
+
return childEnv;
|
|
64
|
+
}
|
|
52
65
|
function powershell(script, env) {
|
|
53
66
|
const root = process.env.SystemRoot ?? "C:\\Windows";
|
|
54
67
|
// A fixed system path, so a powershell.exe earlier on PATH is never used.
|
|
55
68
|
const executable = join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
56
|
-
|
|
57
|
-
// 5.1 from loading its own Get-Acl module; 5.1 rebuilds it when unset.
|
|
58
|
-
const childEnv = { ...process.env, ...env };
|
|
59
|
-
for (const key of Object.keys(childEnv))
|
|
60
|
-
if (key.toLowerCase() === "psmodulepath")
|
|
61
|
-
delete childEnv[key];
|
|
69
|
+
const childEnv = powershellEnv(process.env, env);
|
|
62
70
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
63
71
|
execFile(executable, [
|
|
64
72
|
"-NoProfile",
|
|
@@ -2,6 +2,7 @@ import { lstat } from "node:fs/promises";
|
|
|
2
2
|
import { posix } from "node:path";
|
|
3
3
|
import { CONCURRENCY, STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.js";
|
|
4
4
|
import { prepareRiskCallers, } from "../core/risk-callers.js";
|
|
5
|
+
import { SECRET_NAME_GLOBS } from "../core/secret-path.js";
|
|
5
6
|
import { admitGitPath, checkFileAdmission, openRepoFile, resolveInsideRepo, } from "./files.js";
|
|
6
7
|
import { loadSyntaxRootParser } from "./syntax.js";
|
|
7
8
|
import { decodeUtf8, verifyGitUtf8 } from "./utf8.js";
|
|
@@ -151,6 +152,8 @@ export async function collectRiskCallers(exec, options, units, changedFiles = []
|
|
|
151
152
|
ref,
|
|
152
153
|
"--",
|
|
153
154
|
...extensions.map((extension) => `*.${extension}`),
|
|
155
|
+
// Base-tree secret-named files are never searched or read.
|
|
156
|
+
...SECRET_NAME_GLOBS.map((glob) => `:(exclude,icase,glob)**/${glob}`),
|
|
154
157
|
], { cwd, timeout: TIMEOUT_MS, signal: options.signal }).catch((error) => ({
|
|
155
158
|
stdout: "",
|
|
156
159
|
stderr: String(error),
|
package/dist/adapters/shell.js
CHANGED
|
@@ -47,20 +47,31 @@ export function windowsBashCandidates(env) {
|
|
|
47
47
|
return [...new Set(candidates)].filter((candidate) => win32.isAbsolute(candidate) && !isWslLauncher(candidate));
|
|
48
48
|
}
|
|
49
49
|
const UNAVAILABLE = "No permitted bash found: install Git for Windows or set JEV_TOOLS_BASH to the full path of a bash that is not the WSL launcher.";
|
|
50
|
+
/** Never handed to command children: the Jev API key stays with the extension. */
|
|
51
|
+
export const API_KEY_VARIABLE = "JEV_TOOLS_API_KEY";
|
|
52
|
+
/** Copy of `env` without the Jev API key (any case, for Windows). */
|
|
53
|
+
export function withoutApiKey(env) {
|
|
54
|
+
const copy = { ...env };
|
|
55
|
+
for (const key of Object.keys(copy))
|
|
56
|
+
if (key.toUpperCase() === API_KEY_VARIABLE)
|
|
57
|
+
delete copy[key];
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
50
60
|
let cached;
|
|
51
61
|
/**
|
|
52
|
-
* POSIX hosts
|
|
53
|
-
* `bash` is the WSL launcher, so resolve Git for Windows
|
|
54
|
-
* JEV_TOOLS_BASH) to an absolute, permitted path and
|
|
55
|
-
* script. When none is found this fails closed: no
|
|
56
|
-
* because spawning one would search PATH again without
|
|
62
|
+
* POSIX hosts run `env -u JEV_TOOLS_API_KEY CI=1 bash -c`. Windows has no
|
|
63
|
+
* `env` and its default `bash` is the WSL launcher, so resolve Git for Windows
|
|
64
|
+
* bash (or JEV_TOOLS_BASH) to an absolute, permitted path, and unset the key
|
|
65
|
+
* and export CI inside the script. When none is found this fails closed: no
|
|
66
|
+
* bare name is returned, because spawning one would search PATH again without
|
|
67
|
+
* the WSL exclusion.
|
|
57
68
|
*/
|
|
58
69
|
export function resolveShell(platform = process.platform, env = process.env, isExecutable = executable) {
|
|
59
70
|
if (platform !== "win32")
|
|
60
71
|
return {
|
|
61
72
|
ok: true,
|
|
62
73
|
executable: "env",
|
|
63
|
-
prefix: ["CI=1", "bash"],
|
|
74
|
+
prefix: ["-u", API_KEY_VARIABLE, "CI=1", "bash"],
|
|
64
75
|
scriptPrefix: "",
|
|
65
76
|
};
|
|
66
77
|
const useCache = isExecutable === executable && env === process.env;
|
|
@@ -68,7 +79,12 @@ export function resolveShell(platform = process.platform, env = process.env, isE
|
|
|
68
79
|
return cached;
|
|
69
80
|
const found = windowsBashCandidates(env).find((path) => isExecutable(path));
|
|
70
81
|
const resolved = found
|
|
71
|
-
? {
|
|
82
|
+
? {
|
|
83
|
+
ok: true,
|
|
84
|
+
executable: found,
|
|
85
|
+
prefix: [],
|
|
86
|
+
scriptPrefix: `unset ${API_KEY_VARIABLE}; export CI=1; `,
|
|
87
|
+
}
|
|
72
88
|
: { ok: false, error: UNAVAILABLE };
|
|
73
89
|
if (useCache && found)
|
|
74
90
|
cached = resolved;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lstat } from "node:fs/promises";
|
|
2
2
|
import { TEST_SOURCE_MAX_BYTES, TIMEOUT_MS } from "../constants.js";
|
|
3
|
+
import { isSecretPath } from "../core/secret-path.js";
|
|
3
4
|
import { checkFileAdmission, openRepoFile, resolveInsideRepo, } from "./files.js";
|
|
4
5
|
import { decodeUtf8 } from "./utf8.js";
|
|
5
6
|
export async function collectTestInventory(exec, cwd, signal) {
|
|
@@ -11,6 +12,7 @@ export async function collectTestInventory(exec, cwd, signal) {
|
|
|
11
12
|
if (root.code !== 0 || root.killed)
|
|
12
13
|
return {
|
|
13
14
|
ok: false,
|
|
15
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
14
16
|
error: root.stderr || "Cannot identify repository root.",
|
|
15
17
|
};
|
|
16
18
|
cwd = root.stdout.replace(/\n$/, "");
|
|
@@ -25,22 +27,28 @@ export async function collectTestInventory(exec, cwd, signal) {
|
|
|
25
27
|
if (listed.code !== 0 || listed.killed)
|
|
26
28
|
return {
|
|
27
29
|
ok: false,
|
|
30
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
28
31
|
error: listed.stderr || "Cannot inventory repository files.",
|
|
29
32
|
};
|
|
30
33
|
const ignored = await exec("git", ["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"], { cwd, timeout: TIMEOUT_MS, signal });
|
|
31
34
|
if (ignored.code !== 0 || ignored.killed)
|
|
32
35
|
return {
|
|
33
36
|
ok: false,
|
|
37
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
34
38
|
error: ignored.stderr || "Cannot identify ignored tracked files.",
|
|
35
39
|
};
|
|
36
40
|
const excluded = new Set(ignored.stdout.split("\0").filter(Boolean));
|
|
37
|
-
const
|
|
41
|
+
const listedPaths = listed.stdout
|
|
38
42
|
.split("\0")
|
|
39
43
|
.filter((path) => path &&
|
|
40
44
|
(!excluded.has(path) ||
|
|
41
45
|
/(?:^|\/)(?:package|tsconfig[^/]*)\.json$/.test(path)));
|
|
46
|
+
// Secret-named files are named exclusions, never inventory members to read.
|
|
47
|
+
const paths = listedPaths.filter((path) => !isSecretPath(path));
|
|
42
48
|
const admitted = new Set(paths);
|
|
43
|
-
const limits =
|
|
49
|
+
const limits = listedPaths
|
|
50
|
+
.filter(isSecretPath)
|
|
51
|
+
.map((path) => ({ path, kind: "secret_pattern" }));
|
|
44
52
|
const cache = new Map();
|
|
45
53
|
for (let offset = 0; offset < paths.length; offset += 16) {
|
|
46
54
|
const inspected = await Promise.all(paths.slice(offset, offset + 16).map(async (path) => {
|
package/dist/configuration.js
CHANGED
|
@@ -13,23 +13,33 @@ function nonempty(value) {
|
|
|
13
13
|
}
|
|
14
14
|
function validate(values) {
|
|
15
15
|
let validUrl = false;
|
|
16
|
+
let insecureTransport = false;
|
|
16
17
|
try {
|
|
17
18
|
const url = new URL(values.url);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const host = url.hostname.toLowerCase();
|
|
20
|
+
const loopback = host === "127.0.0.1" ||
|
|
21
|
+
host === "localhost" ||
|
|
22
|
+
host === "::1" ||
|
|
23
|
+
host === "[::1]";
|
|
24
|
+
const wellFormed = /^https?:\/\//i.test(values.url) &&
|
|
25
|
+
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
26
|
+
url.hostname !== "" &&
|
|
27
|
+
url.username === "" &&
|
|
28
|
+
url.password === "";
|
|
29
|
+
insecureTransport = wellFormed && url.protocol === "http:" && !loopback;
|
|
30
|
+
validUrl = wellFormed && (url.protocol === "https:" || loopback);
|
|
24
31
|
}
|
|
25
32
|
catch {
|
|
26
33
|
// Keep user-controlled URL and credentials out of error messages.
|
|
27
34
|
}
|
|
35
|
+
if (insecureTransport) {
|
|
36
|
+
throw new Error("Jev configuration refuses plain http: to a non-loopback host; use https: or an http: loopback URL (127.0.0.1, ::1 or localhost).");
|
|
37
|
+
}
|
|
28
38
|
if (!validUrl ||
|
|
29
39
|
!nonempty(values.apiKey) ||
|
|
30
40
|
/[^\x20-\x7e\x80-\xff]/.test(values.apiKey) ||
|
|
31
41
|
!nonempty(values.model)) {
|
|
32
|
-
throw new Error("Jev configuration requires a full
|
|
42
|
+
throw new Error("Jev configuration requires a full https: URL (or http: to 127.0.0.1, ::1 or localhost) without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.");
|
|
33
43
|
}
|
|
34
44
|
}
|
|
35
45
|
/** Effective configuration and its client move together, only after a save succeeds. */
|
package/dist/constants.js
CHANGED
|
@@ -109,11 +109,32 @@ export const GIT_BLOB_BATCH_SIZE = 400;
|
|
|
109
109
|
export const RUNNER_PACKAGE_MAX_BYTES = 64 * 1024;
|
|
110
110
|
// ADR 0001: allowed local dependencies, including erased type imports.
|
|
111
111
|
export const IMPORT_LAYERS = {
|
|
112
|
-
core: [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
core: [
|
|
113
|
+
"core",
|
|
114
|
+
"constants.ts",
|
|
115
|
+
"result.ts",
|
|
116
|
+
"result-types.ts",
|
|
117
|
+
"jev/types.ts",
|
|
118
|
+
],
|
|
119
|
+
presets: [
|
|
120
|
+
"presets",
|
|
121
|
+
"core",
|
|
122
|
+
"constants.ts",
|
|
123
|
+
"result.ts",
|
|
124
|
+
"result-types.ts",
|
|
125
|
+
"jev/types.ts",
|
|
126
|
+
],
|
|
127
|
+
adapters: [
|
|
128
|
+
"adapters",
|
|
129
|
+
"core",
|
|
130
|
+
"constants.ts",
|
|
131
|
+
"result.ts",
|
|
132
|
+
"result-types.ts",
|
|
133
|
+
"jev/types.ts",
|
|
134
|
+
],
|
|
135
|
+
jev: ["jev", "core", "constants.ts", "result.ts", "result-types.ts"],
|
|
116
136
|
texts: ["texts", "constants.ts", "presets"],
|
|
117
137
|
"constants.ts": [],
|
|
118
|
-
"result.ts": [],
|
|
138
|
+
"result.ts": ["result-types.ts"],
|
|
139
|
+
"result-types.ts": [],
|
|
119
140
|
};
|