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.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +22 -14
  4. package/SECURITY.md +17 -1
  5. package/dist/adapters/ask-files.js +11 -2
  6. package/dist/adapters/ask-proof.js +63 -7
  7. package/dist/adapters/command.js +82 -29
  8. package/dist/adapters/docs.js +30 -10
  9. package/dist/adapters/evidence-context.js +119 -0
  10. package/dist/adapters/files.js +141 -16
  11. package/dist/adapters/find.js +34 -6
  12. package/dist/adapters/git-base.js +7 -1
  13. package/dist/adapters/git.js +51 -7
  14. package/dist/adapters/locate-file.js +47 -9
  15. package/dist/adapters/private-storage.js +14 -6
  16. package/dist/adapters/risk-callers.js +3 -0
  17. package/dist/adapters/shell.js +23 -7
  18. package/dist/adapters/test-inventory.js +10 -2
  19. package/dist/configuration.js +17 -7
  20. package/dist/constants.js +26 -5
  21. package/dist/core/ask-references.js +193 -109
  22. package/dist/core/asks.js +78 -7
  23. package/dist/core/locate.js +8 -8
  24. package/dist/core/output.js +17 -0
  25. package/dist/core/result-report.js +302 -0
  26. package/dist/core/secret-path.js +34 -0
  27. package/dist/core/state.js +8 -1
  28. package/dist/core/units.js +1 -1
  29. package/dist/jev/client.js +34 -12
  30. package/dist/mcp/protocol.js +50 -27
  31. package/dist/mcp/tools.js +20 -7
  32. package/dist/render.js +72 -0
  33. package/dist/report-schema.js +1356 -0
  34. package/dist/result-types.js +1 -0
  35. package/dist/texts/ask-files.js +3 -1
  36. package/dist/texts/ask.js +3 -1
  37. package/dist/texts/check-diff.js +7 -4
  38. package/dist/texts/find.js +7 -2
  39. package/dist/texts/guide.js +3 -16
  40. package/dist/texts/instructions.js +72 -0
  41. package/dist/texts/locate.js +7 -2
  42. package/dist/texts/select-tests.js +3 -1
  43. package/dist/tools/ask-files.js +248 -15
  44. package/dist/tools/ask.js +523 -62
  45. package/dist/tools/check-diff.js +222 -30
  46. package/dist/tools/docs-check.js +122 -13
  47. package/dist/tools/find.js +320 -27
  48. package/dist/tools/locate.js +317 -18
  49. package/dist/tools/review-report.js +230 -0
  50. package/dist/tools/select-tests.js +273 -19
  51. package/dist/tools/spec-check.js +119 -22
  52. package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
  53. package/docs/agent-instructions.md +59 -30
  54. package/docs/design.md +13 -1
  55. package/docs/mcp.md +8 -6
  56. package/docs/tools/jev_ask.md +8 -5
  57. package/docs/tools/jev_ask_files.md +2 -1
  58. package/docs/tools/jev_check_diff.md +4 -1
  59. package/docs/tools/jev_find_files.md +2 -1
  60. package/docs/tools/jev_locate_in_file.md +5 -0
  61. package/docs/tools/jev_select_tests.md +4 -1
  62. package/package.json +1 -1
  63. package/rules/jev-ask.md +22 -1
  64. package/server.json +2 -2
  65. package/src/adapters/ask-files.ts +11 -3
  66. package/src/adapters/ask-proof.ts +69 -11
  67. package/src/adapters/command.ts +96 -33
  68. package/src/adapters/docs.ts +33 -14
  69. package/src/adapters/evidence-context.ts +169 -0
  70. package/src/adapters/files.ts +146 -16
  71. package/src/adapters/find.ts +37 -7
  72. package/src/adapters/git-base.ts +7 -1
  73. package/src/adapters/git.ts +61 -8
  74. package/src/adapters/locate-file.ts +51 -9
  75. package/src/adapters/private-storage.ts +17 -5
  76. package/src/adapters/risk-callers.ts +3 -0
  77. package/src/adapters/shell.ts +23 -7
  78. package/src/adapters/test-inventory.ts +12 -4
  79. package/src/configuration.ts +16 -2
  80. package/src/constants.ts +26 -5
  81. package/src/core/ask-references.ts +262 -146
  82. package/src/core/asks.ts +79 -7
  83. package/src/core/import-boundaries.ts +8 -3
  84. package/src/core/locate.ts +8 -5
  85. package/src/core/output.ts +34 -0
  86. package/src/core/result-report.ts +410 -0
  87. package/src/core/secret-path.ts +37 -0
  88. package/src/core/state.ts +8 -1
  89. package/src/core/units.ts +3 -2
  90. package/src/index.ts +3 -0
  91. package/src/jev/client.ts +54 -16
  92. package/src/jev/types.ts +18 -3
  93. package/src/mcp/protocol.ts +91 -41
  94. package/src/mcp/tools.ts +26 -13
  95. package/src/render.ts +109 -0
  96. package/src/report-schema.ts +1380 -0
  97. package/src/result-types.ts +234 -0
  98. package/src/result.ts +4 -1
  99. package/src/runtime.ts +6 -0
  100. package/src/texts/ask-files.ts +4 -1
  101. package/src/texts/ask.ts +8 -1
  102. package/src/texts/check-diff.ts +7 -4
  103. package/src/texts/find.ts +8 -2
  104. package/src/texts/guide.ts +8 -16
  105. package/src/texts/instructions.ts +98 -0
  106. package/src/texts/locate.ts +8 -2
  107. package/src/texts/run-end.ts +2 -2
  108. package/src/texts/select-tests.ts +4 -1
  109. package/src/tools/ask-files.ts +309 -14
  110. package/src/tools/ask.ts +700 -77
  111. package/src/tools/check-diff.ts +331 -28
  112. package/src/tools/docs-check.ts +241 -39
  113. package/src/tools/find.ts +386 -29
  114. package/src/tools/locate.ts +384 -19
  115. package/src/tools/review-report.ts +308 -0
  116. package/src/tools/select-tests.ts +479 -21
  117. package/src/tools/spec-check.ts +193 -19
@@ -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
- | { ok: false; kind: "inconsistent_diff"; error: string };
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 { ok: false, error: `Git failed: ${result.stderr}` };
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 { ok: false, error: `Unable to execute git: ${String(error)}` };
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) paths.push(`:(top,exclude,literal)${path}`);
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 { ok: false, error: "Incomplete Git collection." };
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(...candidates.filter((file) => unsupported.includes(file.path)));
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 { ok: false, error: "Incomplete Git base tree; collect again." };
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()) return { ok: false, error: `Not a file: ${path}.` };
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 { ok: false, error: `Not a text file: ${path}.` };
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) return { ok: false, error: `${path}: ${decoded.error}` };
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 { ok: false, error: `Cannot read ${path}: ${String(error)}` };
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 { ok: false, error: `Not a text file: ${path}.` };
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) return { ok: false, error: `${path}: ${decoded.error}` };
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) return { ok: false, error: `${path}: ${end.error}` };
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 { ok: false, error: "Selected range too large." };
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 { ok: false, error: `Cannot read ${path}: ${String(error)}` };
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
- // PowerShell 7 parents export PSModulePath, which stops Windows PowerShell
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) => ({
@@ -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 keep `env CI=1 bash -c`. Windows has no `env` and its default
72
- * `bash` is the WSL launcher, so resolve Git for Windows bash (or
73
- * JEV_TOOLS_BASH) to an absolute, permitted path and export CI inside the
74
- * script. When none is found this fails closed: no bare name is returned,
75
- * because spawning one would search PATH again without the WSL exclusion.
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
- ? { ok: true, executable: found, prefix: [], scriptPrefix: "export CI=1; " }
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 paths = listed.stdout
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(
@@ -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
- validUrl =
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 HTTP(S) URL without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.",
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: ["core", "constants.ts", "result.ts", "jev/types.ts"],
114
- presets: ["presets", "core", "constants.ts", "result.ts", "jev/types.ts"],
115
- adapters: ["adapters", "core", "constants.ts", "result.ts", "jev/types.ts"],
116
- jev: ["jev", "core", "constants.ts", "result.ts"],
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;