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
@@ -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 { ok: false, error: `Git failed: ${result.stderr}` };
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 { ok: false, error: `Unable to execute git: ${String(error)}` };
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 { ok: false, error: "Incomplete Git collection." };
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 { ok: false, error: "Incomplete Git base tree; collect again." };
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 { ok: false, error: `Not a file: ${path}.` };
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 { ok: false, error: `Not a text file: ${path}.` };
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 { ok: false, error: `${path}: ${decoded.error}` };
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 { ok: false, error: `Cannot read ${path}: ${String(error)}` };
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 { ok: false, error: `Not a text file: ${path}.` };
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 { ok: false, error: `${path}: ${decoded.error}` };
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 { ok: false, error: `${path}: ${end.error}` };
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 { ok: false, error: "Selected range too large." };
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 { ok: false, error: `Cannot read ${path}: ${String(error)}` };
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
- // PowerShell 7 parents export PSModulePath, which stops Windows PowerShell
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),
@@ -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 keep `env CI=1 bash -c`. Windows has no `env` and its default
53
- * `bash` is the WSL launcher, so resolve Git for Windows bash (or
54
- * JEV_TOOLS_BASH) to an absolute, permitted path and export CI inside the
55
- * script. When none is found this fails closed: no bare name is returned,
56
- * because spawning one would search PATH again without the WSL exclusion.
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
- ? { ok: true, executable: found, prefix: [], scriptPrefix: "export CI=1; " }
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 paths = listed.stdout
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) => {
@@ -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
- validUrl =
19
- /^https?:\/\//i.test(values.url) &&
20
- (url.protocol === "http:" || url.protocol === "https:") &&
21
- url.hostname !== "" &&
22
- url.username === "" &&
23
- url.password === "";
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 HTTP(S) URL without embedded credentials, a nonempty HTTP-header-compatible API key, and a nonempty model.");
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: ["core", "constants.ts", "result.ts", "jev/types.ts"],
113
- presets: ["presets", "core", "constants.ts", "result.ts", "jev/types.ts"],
114
- adapters: ["adapters", "core", "constants.ts", "result.ts", "jev/types.ts"],
115
- jev: ["jev", "core", "constants.ts", "result.ts"],
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
  };