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/src/adapters/git.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "../constants.ts";
|
|
8
8
|
import { type DiffFile, parseDiff } from "../core/diff.ts";
|
|
9
9
|
import type { GitExec } from "../core/git.ts";
|
|
10
|
+
import { isSecretPath } from "../core/secret-path.ts";
|
|
10
11
|
import {
|
|
11
12
|
buildUnits,
|
|
12
13
|
type EvidenceUnit,
|
|
@@ -22,7 +23,12 @@ import { decodeUtf8, verifyGitUtf8 } from "./utf8.ts";
|
|
|
22
23
|
|
|
23
24
|
export type CollectionResult<T> =
|
|
24
25
|
| Result<T>
|
|
25
|
-
| {
|
|
26
|
+
| {
|
|
27
|
+
ok: false;
|
|
28
|
+
kind: "inconsistent_diff";
|
|
29
|
+
cause: "git_failure";
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
26
32
|
export interface DiffOptions {
|
|
27
33
|
cwd: string;
|
|
28
34
|
base: string;
|
|
@@ -62,13 +68,22 @@ async function git(
|
|
|
62
68
|
if (result.killed)
|
|
63
69
|
return {
|
|
64
70
|
ok: false,
|
|
71
|
+
cause: options.signal?.aborted ? "cancelled" : "git_failure",
|
|
65
72
|
error: "Git interrupted (cancelled or timed out).",
|
|
66
73
|
};
|
|
67
74
|
if (result.code !== 0)
|
|
68
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
cause: "git_failure",
|
|
78
|
+
error: `Git failed: ${result.stderr}`,
|
|
79
|
+
};
|
|
69
80
|
return { ok: true, text: result.stdout };
|
|
70
81
|
} catch (error) {
|
|
71
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
cause: options.signal?.aborted ? "cancelled" : "git_failure",
|
|
85
|
+
error: `Unable to execute git: ${String(error)}`,
|
|
86
|
+
};
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
type ReadSource = {
|
|
@@ -233,13 +248,21 @@ export async function collectDiff(
|
|
|
233
248
|
} catch {
|
|
234
249
|
return {
|
|
235
250
|
ok: false,
|
|
251
|
+
cause: "git_failure",
|
|
236
252
|
error: "Inconsistent Git snapshots; collect again.",
|
|
237
253
|
kind: "inconsistent_diff",
|
|
238
254
|
};
|
|
239
255
|
}
|
|
240
256
|
const unsupported: string[] = [];
|
|
257
|
+
// Secret-named paths (either side of a rename) stay named exclusions;
|
|
258
|
+
// excluding them from every later pathspec keeps Git from emitting their
|
|
259
|
+
// patch text or pairing them into a rename.
|
|
260
|
+
const secret = candidates
|
|
261
|
+
.filter((file) => isSecretPath(file.path) || isSecretPath(file.oldPath))
|
|
262
|
+
.map((file) => file.path);
|
|
241
263
|
await Promise.all(
|
|
242
264
|
candidates.map(async (file) => {
|
|
265
|
+
if (secret.includes(file.path)) return;
|
|
243
266
|
try {
|
|
244
267
|
const stat = await lstat(resolve(cwd, file.path));
|
|
245
268
|
if (
|
|
@@ -254,7 +277,11 @@ export async function collectDiff(
|
|
|
254
277
|
}
|
|
255
278
|
}),
|
|
256
279
|
);
|
|
257
|
-
for (const path of unsupported
|
|
280
|
+
for (const path of [...unsupported, ...secret])
|
|
281
|
+
paths.push(`:(top,exclude,literal)${path}`);
|
|
282
|
+
for (const file of candidates)
|
|
283
|
+
if (secret.includes(file.path) && file.oldPath !== file.path)
|
|
284
|
+
paths.push(`:(top,exclude,literal)${file.oldPath}`);
|
|
258
285
|
const prefix = [
|
|
259
286
|
"diff",
|
|
260
287
|
"--no-color",
|
|
@@ -329,7 +356,11 @@ export async function collectDiff(
|
|
|
329
356
|
!current.ok ||
|
|
330
357
|
!base.ok
|
|
331
358
|
)
|
|
332
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
cause: "git_failure",
|
|
362
|
+
error: "Incomplete Git collection.",
|
|
363
|
+
};
|
|
333
364
|
const currentInventory = new Set(current.text.split("\0").filter(Boolean));
|
|
334
365
|
const baseInventory = new Map<string, string>();
|
|
335
366
|
for (const entry of base.text.split("\0")) {
|
|
@@ -348,17 +379,23 @@ export async function collectDiff(
|
|
|
348
379
|
} catch {
|
|
349
380
|
return {
|
|
350
381
|
ok: false,
|
|
382
|
+
cause: "git_failure",
|
|
351
383
|
error: "Inconsistent Git snapshots; collect again.",
|
|
352
384
|
kind: "inconsistent_diff",
|
|
353
385
|
};
|
|
354
386
|
}
|
|
355
|
-
files.push(
|
|
387
|
+
files.push(
|
|
388
|
+
...candidates.filter(
|
|
389
|
+
(file) => unsupported.includes(file.path) || secret.includes(file.path),
|
|
390
|
+
),
|
|
391
|
+
);
|
|
356
392
|
const objects = new Map<string, { id: string; size: number }>();
|
|
357
393
|
for (const file of files) {
|
|
358
394
|
if (
|
|
359
395
|
/^[A?]/.test(file.status) ||
|
|
360
396
|
file.binary ||
|
|
361
|
-
unsupported.includes(file.path)
|
|
397
|
+
unsupported.includes(file.path) ||
|
|
398
|
+
secret.includes(file.path)
|
|
362
399
|
)
|
|
363
400
|
continue;
|
|
364
401
|
const admission = await admitGitPath(
|
|
@@ -387,6 +424,18 @@ export async function collectDiff(
|
|
|
387
424
|
const collectFile = async (
|
|
388
425
|
file: DiffFile,
|
|
389
426
|
): Promise<CollectionResult<{ file: SourceFile }>> => {
|
|
427
|
+
if (secret.includes(file.path))
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
file: {
|
|
431
|
+
...file,
|
|
432
|
+
hunks: [],
|
|
433
|
+
binary: false,
|
|
434
|
+
before: null,
|
|
435
|
+
after: null,
|
|
436
|
+
limitation: "secret_pattern",
|
|
437
|
+
},
|
|
438
|
+
};
|
|
390
439
|
const historical = !/^[A?]/.test(file.status);
|
|
391
440
|
const admission = historical
|
|
392
441
|
? await admitGitPath(
|
|
@@ -453,7 +502,11 @@ export async function collectDiff(
|
|
|
453
502
|
if (!/^[A?]/.test(file.status)) {
|
|
454
503
|
const object = objects.get(file.oldPath);
|
|
455
504
|
if (!object)
|
|
456
|
-
return {
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
cause: "git_failure",
|
|
508
|
+
error: "Incomplete Git base tree; collect again.",
|
|
509
|
+
};
|
|
457
510
|
if (object.size > STATE_MAX_CHARS) limitation = "too_large";
|
|
458
511
|
else if (loaded.invalid.has(object.id)) limitation = "not UTF-8 text";
|
|
459
512
|
else before = loaded.blobs.get(object.id) ?? "";
|
|
@@ -34,6 +34,7 @@ export async function readLocateFile(
|
|
|
34
34
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(path))
|
|
35
35
|
return {
|
|
36
36
|
ok: false,
|
|
37
|
+
cause: "forbidden_path",
|
|
37
38
|
error: `Internal URLs are not files; use read for ${path}.`,
|
|
38
39
|
};
|
|
39
40
|
const opened = await openRepoFile(cwd, path, { exec, signal });
|
|
@@ -41,7 +42,12 @@ export async function readLocateFile(
|
|
|
41
42
|
const handle = opened.handle;
|
|
42
43
|
try {
|
|
43
44
|
const stat = await handle.stat();
|
|
44
|
-
if (!stat.isFile())
|
|
45
|
+
if (!stat.isFile())
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
cause: "file_unavailable",
|
|
49
|
+
error: `Not a file: ${path}.`,
|
|
50
|
+
};
|
|
45
51
|
const buffer = Buffer.alloc(
|
|
46
52
|
Math.min(stat.size + 1, LOCATE_WHOLE_MAX_BYTES + 1),
|
|
47
53
|
);
|
|
@@ -58,10 +64,19 @@ export async function readLocateFile(
|
|
|
58
64
|
count += read.bytesRead;
|
|
59
65
|
}
|
|
60
66
|
if (buffer.subarray(0, count).includes(0))
|
|
61
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
cause: "binary_or_non_utf8",
|
|
70
|
+
error: `Not a text file: ${path}.`,
|
|
71
|
+
};
|
|
62
72
|
if (count < buffer.length) {
|
|
63
73
|
const decoded = decodeUtf8(buffer.subarray(0, count));
|
|
64
|
-
if (!decoded.ok)
|
|
74
|
+
if (!decoded.ok)
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
cause: "binary_or_non_utf8",
|
|
78
|
+
error: `${path}: ${decoded.error}`,
|
|
79
|
+
};
|
|
65
80
|
const text = decoded.text;
|
|
66
81
|
if (text.length <= LOCATE_WHOLE_MAX_CHARS)
|
|
67
82
|
return {
|
|
@@ -85,7 +100,11 @@ export async function readLocateFile(
|
|
|
85
100
|
}
|
|
86
101
|
: scan;
|
|
87
102
|
} catch (error) {
|
|
88
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
106
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
107
|
+
};
|
|
89
108
|
} finally {
|
|
90
109
|
await handle.close();
|
|
91
110
|
}
|
|
@@ -171,26 +190,49 @@ export async function scanRange(
|
|
|
171
190
|
break;
|
|
172
191
|
}
|
|
173
192
|
if (buffer.subarray(0, read.bytesRead).includes(0))
|
|
174
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
cause: "binary_or_non_utf8",
|
|
196
|
+
error: `Not a text file: ${path}.`,
|
|
197
|
+
};
|
|
175
198
|
bytes += read.bytesRead;
|
|
176
199
|
const decoded = decode(buffer.subarray(0, read.bytesRead));
|
|
177
|
-
if (!decoded.ok)
|
|
200
|
+
if (!decoded.ok)
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
cause: "binary_or_non_utf8",
|
|
204
|
+
error: `${path}: ${decoded.error}`,
|
|
205
|
+
};
|
|
178
206
|
if (!consume(decoded.text))
|
|
179
207
|
return {
|
|
180
208
|
ok: false,
|
|
209
|
+
cause: "evidence_too_large",
|
|
181
210
|
error: `Selected range exceeds ${STATE_MAX_CHARS} chars; read a narrower range directly.`,
|
|
182
211
|
};
|
|
183
212
|
if (range && line > range.end) break;
|
|
184
213
|
}
|
|
185
214
|
const end = reachedEof ? decode() : { ok: true as const, text: "" };
|
|
186
|
-
if (!end.ok)
|
|
215
|
+
if (!end.ok)
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
cause: "binary_or_non_utf8",
|
|
219
|
+
error: `${path}: ${end.error}`,
|
|
220
|
+
};
|
|
187
221
|
if (!consume(end.text))
|
|
188
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
cause: "evidence_too_large",
|
|
225
|
+
error: "Selected range too large.",
|
|
226
|
+
};
|
|
189
227
|
const lines = line - Number(!pending);
|
|
190
228
|
if (lines > 0 && (windows.at(-1)?.end ?? 0) < lines) finishWindow(lines);
|
|
191
229
|
return { ok: true, bytes, lines, windows, text };
|
|
192
230
|
} catch (error) {
|
|
193
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
cause: signal?.aborted ? "cancelled" : "file_unavailable",
|
|
234
|
+
error: `Cannot read ${path}: ${String(error)}`,
|
|
235
|
+
};
|
|
194
236
|
} finally {
|
|
195
237
|
await handle.close();
|
|
196
238
|
}
|
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import type { Stats } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { PRIVATE_STORAGE_TIMEOUT_MS } from "../constants.ts";
|
|
5
|
+
import { withoutApiKey } from "./shell.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Owner-only storage, checked with each operating system's own model.
|
|
@@ -77,6 +78,21 @@ foreach ($id in @($me.Value, '${SYSTEM}', '${ADMINISTRATORS}')) {
|
|
|
77
78
|
}
|
|
78
79
|
Set-Acl -LiteralPath $path -AclObject $acl`;
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Host environment for the ACL helper, without the Jev API key. PowerShell 7
|
|
83
|
+
* parents export PSModulePath, which stops Windows PowerShell 5.1 from loading
|
|
84
|
+
* its own Get-Acl module; 5.1 rebuilds it when unset.
|
|
85
|
+
*/
|
|
86
|
+
export function powershellEnv(
|
|
87
|
+
host: NodeJS.ProcessEnv,
|
|
88
|
+
env: Record<string, string>,
|
|
89
|
+
): NodeJS.ProcessEnv {
|
|
90
|
+
const childEnv = withoutApiKey({ ...host, ...env });
|
|
91
|
+
for (const key of Object.keys(childEnv))
|
|
92
|
+
if (key.toLowerCase() === "psmodulepath") delete childEnv[key];
|
|
93
|
+
return childEnv;
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
function powershell(
|
|
81
97
|
script: string,
|
|
82
98
|
env: Record<string, string>,
|
|
@@ -90,11 +106,7 @@ function powershell(
|
|
|
90
106
|
"v1.0",
|
|
91
107
|
"powershell.exe",
|
|
92
108
|
);
|
|
93
|
-
|
|
94
|
-
// 5.1 from loading its own Get-Acl module; 5.1 rebuilds it when unset.
|
|
95
|
-
const childEnv: NodeJS.ProcessEnv = { ...process.env, ...env };
|
|
96
|
-
for (const key of Object.keys(childEnv))
|
|
97
|
-
if (key.toLowerCase() === "psmodulepath") delete childEnv[key];
|
|
109
|
+
const childEnv = powershellEnv(process.env, env);
|
|
98
110
|
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
99
111
|
execFile(
|
|
100
112
|
executable,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type CallerSource,
|
|
10
10
|
prepareRiskCallers,
|
|
11
11
|
} from "../core/risk-callers.ts";
|
|
12
|
+
import { SECRET_NAME_GLOBS } from "../core/secret-path.ts";
|
|
12
13
|
import type { SyntaxRootParser } from "../core/syntax.ts";
|
|
13
14
|
import type { EvidenceUnit, SourceFile } from "../core/units.ts";
|
|
14
15
|
import {
|
|
@@ -201,6 +202,8 @@ export async function collectRiskCallers(
|
|
|
201
202
|
ref,
|
|
202
203
|
"--",
|
|
203
204
|
...extensions.map((extension) => `*.${extension}`),
|
|
205
|
+
// Base-tree secret-named files are never searched or read.
|
|
206
|
+
...SECRET_NAME_GLOBS.map((glob) => `:(exclude,icase,glob)**/${glob}`),
|
|
204
207
|
],
|
|
205
208
|
{ cwd, timeout: TIMEOUT_MS, signal: options.signal },
|
|
206
209
|
).catch((error: unknown) => ({
|
package/src/adapters/shell.ts
CHANGED
|
@@ -65,14 +65,25 @@ export function windowsBashCandidates(env: NodeJS.ProcessEnv): string[] {
|
|
|
65
65
|
|
|
66
66
|
const UNAVAILABLE =
|
|
67
67
|
"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.";
|
|
68
|
+
/** Never handed to command children: the Jev API key stays with the extension. */
|
|
69
|
+
export const API_KEY_VARIABLE = "JEV_TOOLS_API_KEY";
|
|
70
|
+
|
|
71
|
+
/** Copy of `env` without the Jev API key (any case, for Windows). */
|
|
72
|
+
export function withoutApiKey(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
73
|
+
const copy = { ...env };
|
|
74
|
+
for (const key of Object.keys(copy))
|
|
75
|
+
if (key.toUpperCase() === API_KEY_VARIABLE) delete copy[key];
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
68
78
|
let cached: ShellResolution | undefined;
|
|
69
79
|
|
|
70
80
|
/**
|
|
71
|
-
* POSIX hosts
|
|
72
|
-
* `bash` is the WSL launcher, so resolve Git for Windows
|
|
73
|
-
* JEV_TOOLS_BASH) to an absolute, permitted path and
|
|
74
|
-
* script. When none is found this fails closed: no
|
|
75
|
-
* because spawning one would search PATH again without
|
|
81
|
+
* POSIX hosts run `env -u JEV_TOOLS_API_KEY CI=1 bash -c`. Windows has no
|
|
82
|
+
* `env` and its default `bash` is the WSL launcher, so resolve Git for Windows
|
|
83
|
+
* bash (or JEV_TOOLS_BASH) to an absolute, permitted path, and unset the key
|
|
84
|
+
* and export CI inside the script. When none is found this fails closed: no
|
|
85
|
+
* bare name is returned, because spawning one would search PATH again without
|
|
86
|
+
* the WSL exclusion.
|
|
76
87
|
*/
|
|
77
88
|
export function resolveShell(
|
|
78
89
|
platform: NodeJS.Platform = process.platform,
|
|
@@ -83,14 +94,19 @@ export function resolveShell(
|
|
|
83
94
|
return {
|
|
84
95
|
ok: true,
|
|
85
96
|
executable: "env",
|
|
86
|
-
prefix: ["CI=1", "bash"],
|
|
97
|
+
prefix: ["-u", API_KEY_VARIABLE, "CI=1", "bash"],
|
|
87
98
|
scriptPrefix: "",
|
|
88
99
|
};
|
|
89
100
|
const useCache = isExecutable === executable && env === process.env;
|
|
90
101
|
if (useCache && cached) return cached;
|
|
91
102
|
const found = windowsBashCandidates(env).find((path) => isExecutable(path));
|
|
92
103
|
const resolved: ShellResolution = found
|
|
93
|
-
? {
|
|
104
|
+
? {
|
|
105
|
+
ok: true,
|
|
106
|
+
executable: found,
|
|
107
|
+
prefix: [],
|
|
108
|
+
scriptPrefix: `unset ${API_KEY_VARIABLE}; export CI=1; `,
|
|
109
|
+
}
|
|
94
110
|
: { ok: false, error: UNAVAILABLE };
|
|
95
111
|
if (useCache && found) cached = resolved;
|
|
96
112
|
return resolved;
|
|
@@ -2,6 +2,7 @@ import { type FileHandle, lstat } from "node:fs/promises";
|
|
|
2
2
|
import { TEST_SOURCE_MAX_BYTES, TIMEOUT_MS } from "../constants.ts";
|
|
3
3
|
import type { GitExec } from "../core/git.ts";
|
|
4
4
|
import type { ImportSource } from "../core/imports.ts";
|
|
5
|
+
import { isSecretPath } from "../core/secret-path.ts";
|
|
5
6
|
import type { Result } from "../result.ts";
|
|
6
7
|
import {
|
|
7
8
|
checkFileAdmission,
|
|
@@ -21,7 +22,7 @@ export async function collectTestInventory(
|
|
|
21
22
|
read(path: string): Promise<ImportSource | undefined>;
|
|
22
23
|
limits: readonly {
|
|
23
24
|
path: string;
|
|
24
|
-
kind: "unreadable" | "too_large" | "not UTF-8 text";
|
|
25
|
+
kind: "unreadable" | "too_large" | "not UTF-8 text" | "secret_pattern";
|
|
25
26
|
}[];
|
|
26
27
|
}>
|
|
27
28
|
> {
|
|
@@ -33,6 +34,7 @@ export async function collectTestInventory(
|
|
|
33
34
|
if (root.code !== 0 || root.killed)
|
|
34
35
|
return {
|
|
35
36
|
ok: false,
|
|
37
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
36
38
|
error: root.stderr || "Cannot identify repository root.",
|
|
37
39
|
};
|
|
38
40
|
cwd = root.stdout.replace(/\n$/, "");
|
|
@@ -51,6 +53,7 @@ export async function collectTestInventory(
|
|
|
51
53
|
if (listed.code !== 0 || listed.killed)
|
|
52
54
|
return {
|
|
53
55
|
ok: false,
|
|
56
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
54
57
|
error: listed.stderr || "Cannot inventory repository files.",
|
|
55
58
|
};
|
|
56
59
|
const ignored = await exec(
|
|
@@ -61,10 +64,11 @@ export async function collectTestInventory(
|
|
|
61
64
|
if (ignored.code !== 0 || ignored.killed)
|
|
62
65
|
return {
|
|
63
66
|
ok: false,
|
|
67
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
64
68
|
error: ignored.stderr || "Cannot identify ignored tracked files.",
|
|
65
69
|
};
|
|
66
70
|
const excluded = new Set(ignored.stdout.split("\0").filter(Boolean));
|
|
67
|
-
const
|
|
71
|
+
const listedPaths = listed.stdout
|
|
68
72
|
.split("\0")
|
|
69
73
|
.filter(
|
|
70
74
|
(path) =>
|
|
@@ -72,11 +76,15 @@ export async function collectTestInventory(
|
|
|
72
76
|
(!excluded.has(path) ||
|
|
73
77
|
/(?:^|\/)(?:package|tsconfig[^/]*)\.json$/.test(path)),
|
|
74
78
|
);
|
|
79
|
+
// Secret-named files are named exclusions, never inventory members to read.
|
|
80
|
+
const paths = listedPaths.filter((path) => !isSecretPath(path));
|
|
75
81
|
const admitted = new Set(paths);
|
|
76
82
|
const limits: {
|
|
77
83
|
path: string;
|
|
78
|
-
kind: "unreadable" | "too_large" | "not UTF-8 text";
|
|
79
|
-
}[] =
|
|
84
|
+
kind: "unreadable" | "too_large" | "not UTF-8 text" | "secret_pattern";
|
|
85
|
+
}[] = listedPaths
|
|
86
|
+
.filter(isSecretPath)
|
|
87
|
+
.map((path) => ({ path, kind: "secret_pattern" as const }));
|
|
80
88
|
const cache = new Map<string, Promise<ImportSource | undefined>>();
|
|
81
89
|
for (let offset = 0; offset < paths.length; offset += 16) {
|
|
82
90
|
const inspected = await Promise.all(
|
package/src/configuration.ts
CHANGED
|
@@ -30,17 +30,31 @@ function nonempty(value: string | undefined): value is string {
|
|
|
30
30
|
|
|
31
31
|
function validate(values: ConfigurationValues): void {
|
|
32
32
|
let validUrl = false;
|
|
33
|
+
let insecureTransport = false;
|
|
33
34
|
try {
|
|
34
35
|
const url = new URL(values.url);
|
|
35
|
-
|
|
36
|
+
const host = url.hostname.toLowerCase();
|
|
37
|
+
const loopback =
|
|
38
|
+
host === "127.0.0.1" ||
|
|
39
|
+
host === "localhost" ||
|
|
40
|
+
host === "::1" ||
|
|
41
|
+
host === "[::1]";
|
|
42
|
+
const wellFormed =
|
|
36
43
|
/^https?:\/\//i.test(values.url) &&
|
|
37
44
|
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
38
45
|
url.hostname !== "" &&
|
|
39
46
|
url.username === "" &&
|
|
40
47
|
url.password === "";
|
|
48
|
+
insecureTransport = wellFormed && url.protocol === "http:" && !loopback;
|
|
49
|
+
validUrl = wellFormed && (url.protocol === "https:" || loopback);
|
|
41
50
|
} catch {
|
|
42
51
|
// Keep user-controlled URL and credentials out of error messages.
|
|
43
52
|
}
|
|
53
|
+
if (insecureTransport) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Jev configuration refuses plain http: to a non-loopback host; use https: or an http: loopback URL (127.0.0.1, ::1 or localhost).",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
44
58
|
if (
|
|
45
59
|
!validUrl ||
|
|
46
60
|
!nonempty(values.apiKey) ||
|
|
@@ -48,7 +62,7 @@ function validate(values: ConfigurationValues): void {
|
|
|
48
62
|
!nonempty(values.model)
|
|
49
63
|
) {
|
|
50
64
|
throw new Error(
|
|
51
|
-
"Jev configuration requires a full
|
|
65
|
+
"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.",
|
|
52
66
|
);
|
|
53
67
|
}
|
|
54
68
|
}
|
package/src/constants.ts
CHANGED
|
@@ -110,11 +110,32 @@ export const RUNNER_PACKAGE_MAX_BYTES = 64 * 1024;
|
|
|
110
110
|
|
|
111
111
|
// ADR 0001: allowed local dependencies, including erased type imports.
|
|
112
112
|
export const IMPORT_LAYERS = {
|
|
113
|
-
core: [
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
core: [
|
|
114
|
+
"core",
|
|
115
|
+
"constants.ts",
|
|
116
|
+
"result.ts",
|
|
117
|
+
"result-types.ts",
|
|
118
|
+
"jev/types.ts",
|
|
119
|
+
],
|
|
120
|
+
presets: [
|
|
121
|
+
"presets",
|
|
122
|
+
"core",
|
|
123
|
+
"constants.ts",
|
|
124
|
+
"result.ts",
|
|
125
|
+
"result-types.ts",
|
|
126
|
+
"jev/types.ts",
|
|
127
|
+
],
|
|
128
|
+
adapters: [
|
|
129
|
+
"adapters",
|
|
130
|
+
"core",
|
|
131
|
+
"constants.ts",
|
|
132
|
+
"result.ts",
|
|
133
|
+
"result-types.ts",
|
|
134
|
+
"jev/types.ts",
|
|
135
|
+
],
|
|
136
|
+
jev: ["jev", "core", "constants.ts", "result.ts", "result-types.ts"],
|
|
117
137
|
texts: ["texts", "constants.ts", "presets"],
|
|
118
138
|
"constants.ts": [],
|
|
119
|
-
"result.ts": [],
|
|
139
|
+
"result.ts": ["result-types.ts"],
|
|
140
|
+
"result-types.ts": [],
|
|
120
141
|
} as const;
|