nexus-ai-pro 2.3.0 → 2.4.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 (178) hide show
  1. package/API_STABILITY.md +186 -0
  2. package/CHANGELOG.md +184 -1
  3. package/README.md +14 -5
  4. package/SECURITY.md +140 -0
  5. package/dist/adapter-kit/contracts.d.ts +87 -0
  6. package/dist/adapter-kit/contracts.js +237 -0
  7. package/dist/adapter-kit/errors.d.ts +39 -0
  8. package/dist/adapter-kit/errors.js +52 -0
  9. package/dist/adapter-kit/index.d.ts +144 -0
  10. package/dist/adapter-kit/index.js +178 -0
  11. package/dist/adapter-kit/semver.d.ts +12 -0
  12. package/dist/adapter-kit/semver.js +55 -0
  13. package/dist/agent/middleware/pii.js +2 -33
  14. package/dist/core/secure-stream.js +4 -3
  15. package/dist/embeddings/adapters.js +2 -1
  16. package/dist/evals/judge.js +2 -5
  17. package/dist/evaluate/cache.d.ts +0 -14
  18. package/dist/evaluate/cache.js +0 -30
  19. package/dist/evaluate/file-cache.d.ts +15 -0
  20. package/dist/evaluate/file-cache.js +30 -0
  21. package/dist/evaluate/index.d.ts +2 -1
  22. package/dist/evaluate/index.js +2 -1
  23. package/dist/evaluate/review.js +2 -2
  24. package/dist/evaluate/run.js +2 -2
  25. package/dist/evaluate/version.js +3 -3
  26. package/dist/graph/graph.js +2 -2
  27. package/dist/graph/node-cache.js +3 -3
  28. package/dist/hallucination/rag.d.ts +4 -1
  29. package/dist/hallucination/rag.js +6 -1
  30. package/dist/images/asset-stores.js +2 -3
  31. package/dist/images/asset-support.js +2 -2
  32. package/dist/images/assets.js +1 -2
  33. package/dist/images/evals.js +4 -4
  34. package/dist/insights/proposals.js +1 -2
  35. package/dist/insights/regressions.d.ts +26 -2
  36. package/dist/insights/regressions.js +129 -3
  37. package/dist/loaders/git.js +8 -1
  38. package/dist/loaders/web.js +7 -1
  39. package/dist/mcp/client.d.ts +14 -0
  40. package/dist/mcp/client.js +59 -1
  41. package/dist/mcp/registry.d.ts +6 -0
  42. package/dist/mcp/registry.js +7 -0
  43. package/dist/prompts/file.js +28 -11
  44. package/dist/protocols/a2a.d.ts +235 -0
  45. package/dist/protocols/a2a.js +386 -0
  46. package/dist/protocols/acp.d.ts +87 -0
  47. package/dist/protocols/acp.js +322 -0
  48. package/dist/protocols/ag-ui.d.ts +120 -0
  49. package/dist/protocols/ag-ui.js +180 -0
  50. package/dist/protocols/shared.d.ts +80 -0
  51. package/dist/protocols/shared.js +49 -0
  52. package/dist/providers/anthropic.js +1 -0
  53. package/dist/providers/cohere.js +2 -2
  54. package/dist/providers/google.js +2 -11
  55. package/dist/providers/openai.js +1 -0
  56. package/dist/rag/file-ingestion.js +15 -9
  57. package/dist/runtime/detect.d.ts +17 -0
  58. package/dist/runtime/detect.js +16 -0
  59. package/dist/runtime/index.d.ts +19 -0
  60. package/dist/runtime/index.js +7 -0
  61. package/dist/security/injection-detector.d.ts +2 -0
  62. package/dist/security/injection-detector.js +7 -1
  63. package/dist/security/input-guard.js +5 -2
  64. package/dist/security/output-guard.js +8 -4
  65. package/dist/security/pii-detector.js +22 -6
  66. package/dist/security/prompt-hardening.js +6 -2
  67. package/dist/security/semantic-injection-classifier.d.ts +2 -1
  68. package/dist/security/semantic-injection-classifier.js +31 -35
  69. package/dist/security/upload-scanner.d.ts +8 -0
  70. package/dist/security/upload-scanner.js +51 -9
  71. package/dist/server/deployments.d.ts +63 -1
  72. package/dist/server/deployments.js +75 -2
  73. package/dist/server/runs.d.ts +9 -0
  74. package/dist/server/runs.js +20 -0
  75. package/dist/server/server.js +5 -0
  76. package/dist/testing/record.js +2 -2
  77. package/dist/tracing/stores.js +2 -1
  78. package/dist/tracing/tracer.js +2 -2
  79. package/dist/types/config.d.ts +20 -0
  80. package/dist/types/server.d.ts +6 -0
  81. package/dist/utils/base64.d.ts +4 -0
  82. package/dist/utils/base64.js +21 -0
  83. package/dist/utils/checksums.d.ts +22 -0
  84. package/dist/utils/checksums.js +39 -0
  85. package/dist/utils/ids.d.ts +8 -0
  86. package/dist/utils/ids.js +26 -2
  87. package/dist/utils/sha256.d.ts +22 -0
  88. package/dist/utils/sha256.js +120 -0
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/dist-cjs/adapter-kit/contracts.d.ts +87 -0
  92. package/dist-cjs/adapter-kit/contracts.js +243 -0
  93. package/dist-cjs/adapter-kit/errors.d.ts +39 -0
  94. package/dist-cjs/adapter-kit/errors.js +57 -0
  95. package/dist-cjs/adapter-kit/index.d.ts +144 -0
  96. package/dist-cjs/adapter-kit/index.js +194 -0
  97. package/dist-cjs/adapter-kit/semver.d.ts +12 -0
  98. package/dist-cjs/adapter-kit/semver.js +59 -0
  99. package/dist-cjs/agent/middleware/pii.js +4 -35
  100. package/dist-cjs/core/secure-stream.js +4 -3
  101. package/dist-cjs/embeddings/adapters.js +2 -1
  102. package/dist-cjs/evals/judge.js +2 -5
  103. package/dist-cjs/evaluate/cache.d.ts +0 -14
  104. package/dist-cjs/evaluate/cache.js +1 -65
  105. package/dist-cjs/evaluate/file-cache.d.ts +15 -0
  106. package/dist-cjs/evaluate/file-cache.js +67 -0
  107. package/dist-cjs/evaluate/index.d.ts +2 -1
  108. package/dist-cjs/evaluate/index.js +3 -2
  109. package/dist-cjs/evaluate/review.js +2 -2
  110. package/dist-cjs/evaluate/run.js +2 -2
  111. package/dist-cjs/evaluate/version.js +3 -3
  112. package/dist-cjs/graph/graph.js +2 -2
  113. package/dist-cjs/graph/node-cache.js +3 -3
  114. package/dist-cjs/hallucination/rag.d.ts +4 -1
  115. package/dist-cjs/hallucination/rag.js +6 -1
  116. package/dist-cjs/images/asset-stores.js +2 -3
  117. package/dist-cjs/images/asset-support.js +2 -2
  118. package/dist-cjs/images/assets.js +1 -2
  119. package/dist-cjs/images/evals.js +4 -4
  120. package/dist-cjs/insights/proposals.js +1 -2
  121. package/dist-cjs/insights/regressions.d.ts +26 -2
  122. package/dist-cjs/insights/regressions.js +129 -3
  123. package/dist-cjs/loaders/git.js +8 -1
  124. package/dist-cjs/loaders/web.js +7 -1
  125. package/dist-cjs/mcp/client.d.ts +14 -0
  126. package/dist-cjs/mcp/client.js +60 -1
  127. package/dist-cjs/mcp/registry.d.ts +6 -0
  128. package/dist-cjs/mcp/registry.js +7 -0
  129. package/dist-cjs/prompts/file.js +28 -11
  130. package/dist-cjs/protocols/a2a.d.ts +235 -0
  131. package/dist-cjs/protocols/a2a.js +393 -0
  132. package/dist-cjs/protocols/acp.d.ts +87 -0
  133. package/dist-cjs/protocols/acp.js +326 -0
  134. package/dist-cjs/protocols/ag-ui.d.ts +120 -0
  135. package/dist-cjs/protocols/ag-ui.js +186 -0
  136. package/dist-cjs/protocols/shared.d.ts +80 -0
  137. package/dist-cjs/protocols/shared.js +59 -0
  138. package/dist-cjs/providers/anthropic.js +1 -0
  139. package/dist-cjs/providers/cohere.js +2 -2
  140. package/dist-cjs/providers/google.js +3 -12
  141. package/dist-cjs/providers/openai.js +1 -0
  142. package/dist-cjs/rag/file-ingestion.js +14 -8
  143. package/dist-cjs/runtime/detect.d.ts +17 -0
  144. package/dist-cjs/runtime/detect.js +19 -0
  145. package/dist-cjs/runtime/index.d.ts +19 -0
  146. package/dist-cjs/runtime/index.js +31 -0
  147. package/dist-cjs/security/injection-detector.d.ts +2 -0
  148. package/dist-cjs/security/injection-detector.js +7 -1
  149. package/dist-cjs/security/input-guard.js +5 -2
  150. package/dist-cjs/security/output-guard.js +8 -4
  151. package/dist-cjs/security/pii-detector.js +22 -6
  152. package/dist-cjs/security/prompt-hardening.js +6 -2
  153. package/dist-cjs/security/semantic-injection-classifier.d.ts +2 -1
  154. package/dist-cjs/security/semantic-injection-classifier.js +31 -35
  155. package/dist-cjs/security/upload-scanner.d.ts +8 -0
  156. package/dist-cjs/security/upload-scanner.js +53 -9
  157. package/dist-cjs/server/deployments.d.ts +63 -1
  158. package/dist-cjs/server/deployments.js +75 -2
  159. package/dist-cjs/server/runs.d.ts +9 -0
  160. package/dist-cjs/server/runs.js +20 -0
  161. package/dist-cjs/server/server.js +5 -0
  162. package/dist-cjs/testing/record.js +2 -2
  163. package/dist-cjs/tracing/stores.js +2 -1
  164. package/dist-cjs/tracing/tracer.js +2 -2
  165. package/dist-cjs/types/config.d.ts +20 -0
  166. package/dist-cjs/types/server.d.ts +6 -0
  167. package/dist-cjs/utils/base64.d.ts +4 -0
  168. package/dist-cjs/utils/base64.js +25 -0
  169. package/dist-cjs/utils/checksums.d.ts +22 -0
  170. package/dist-cjs/utils/checksums.js +46 -0
  171. package/dist-cjs/utils/ids.d.ts +8 -0
  172. package/dist-cjs/utils/ids.js +28 -2
  173. package/dist-cjs/utils/sha256.d.ts +22 -0
  174. package/dist-cjs/utils/sha256.js +125 -0
  175. package/dist-cjs/version.d.ts +1 -1
  176. package/dist-cjs/version.js +1 -1
  177. package/llms.txt +5 -2
  178. package/package.json +76 -6
@@ -1,4 +1,4 @@
1
- import { createHash } from 'node:crypto';
1
+ import { Sha256, sha256Hex } from '../utils/sha256.js';
2
2
  import { linkSignals } from '../utils/signals.js';
3
3
  import { ImageValidationError } from './errors.js';
4
4
  export class MemoryReviewQueue {
@@ -325,14 +325,14 @@ function runScores(report) {
325
325
  ];
326
326
  }
327
327
  function mediaVersion(examples) {
328
- const hash = createHash('sha256');
328
+ const hash = new Sha256();
329
329
  for (const example of examples) {
330
330
  hash.update(JSON.stringify({ id: example.id, inputs: example.inputs }, function (key, value) {
331
331
  const original = this[key];
332
- return original instanceof Uint8Array ? `bytes:${createHash('sha256').update(original).digest('hex')}` : value;
332
+ return original instanceof Uint8Array ? `bytes:${sha256Hex(original)}` : value;
333
333
  }));
334
334
  }
335
- return `v${hash.digest('hex').slice(0, 12)}`;
335
+ return `v${hash.digest().slice(0, 12)}`;
336
336
  }
337
337
  function uniqueIds(values) {
338
338
  const seen = new Map();
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'node:crypto';
2
1
  export class MemoryProposalStore {
3
2
  proposals = new Map();
4
3
  save(proposal) {
@@ -93,7 +92,7 @@ export async function proposeFix(options) {
93
92
  ? 'improved'
94
93
  : 'unchanged';
95
94
  const proposal = {
96
- id: `fix-${randomUUID()}`,
95
+ id: `fix-${globalThis.crypto.randomUUID()}`,
97
96
  issueId: options.issue.id,
98
97
  issue: options.issue.summary,
99
98
  kind: subject.kind,
@@ -33,7 +33,10 @@ export interface CompareRunsOptions {
33
33
  metrics?: readonly RegressionMetric[];
34
34
  /** Feedback keys whose mean score is compared, such as `helpfulness`. */
35
35
  feedback?: readonly string[];
36
- /** Runs, or feedback scores, each side needs before it is judged. Defaults to 20. */
36
+ /**
37
+ * Runs each side needs before it is judged; for a feedback score, runs that have one. Defaults to
38
+ * 20. A run counts once per feedback key, however many times it was rated.
39
+ */
37
40
  minRuns?: number;
38
41
  /** A rise in p95 latency, relative, that counts. Defaults to 0.25, a quarter slower. */
39
42
  latencyIncrease?: number;
@@ -44,6 +47,22 @@ export interface CompareRunsOptions {
44
47
  minLatencyChangeMs?: number;
45
48
  /** A rise in mean cost, relative, that counts. Defaults to 0.25. */
46
49
  costIncrease?: number;
50
+ /**
51
+ * Judges every metric the way an evaluation comparison does, at this confidence, such as 0.95. Each
52
+ * metric is resampled by bootstrap, and it regresses only when two things hold: the change's
53
+ * interval does not reach zero, and the change is at least that metric's minimum effect. Noise and
54
+ * trivia both stay below the bar. Without it, the error rate uses a z-test at 95%, and latency and
55
+ * cost use their margins alone.
56
+ */
57
+ confidence?: number;
58
+ /** The smallest rise in the error rate, absolute, that counts under `confidence`. Defaults to 0.02. */
59
+ minErrorRateIncrease?: number;
60
+ /** The smallest fall in a feedback score's mean, absolute, that counts under `confidence`. Defaults to 0.05. */
61
+ minFeedbackDrop?: number;
62
+ /** Bootstrap resamples under `confidence`. Defaults to 1,000. */
63
+ resamples?: number;
64
+ /** Seeds the resampling, so the same runs always get the same verdict. Defaults to 1. */
65
+ seed?: number;
47
66
  }
48
67
  /** Options for `detectRegressions()`. */
49
68
  export interface DetectRegressionsOptions {
@@ -61,7 +80,10 @@ export interface DetectRegressionsOptions {
61
80
  metrics?: readonly RegressionMetric[];
62
81
  /** Feedback keys whose mean score is compared, such as `helpfulness`. */
63
82
  feedback?: readonly string[];
64
- /** Runs, or feedback scores, each window needs before a group is judged. Defaults to 20. */
83
+ /**
84
+ * Runs each window needs before a group is judged; for a feedback score, runs that have one.
85
+ * Defaults to 20.
86
+ */
65
87
  minRuns?: number;
66
88
  /** A rise in p95 latency, relative, that counts. Defaults to 0.25, a quarter slower. */
67
89
  latencyIncrease?: number;
@@ -96,6 +118,8 @@ export interface Regression {
96
118
  };
97
119
  /** One readable line. */
98
120
  summary: string;
121
+ /** The change's interval, in the units of `change`, when the comparison was judged with `confidence`. */
122
+ interval?: [number, number];
99
123
  }
100
124
  /**
101
125
  * Compares a current window of traces with a baseline window and reports what got worse, per run
@@ -1,3 +1,12 @@
1
+ function runScores(runs, key) {
2
+ return runs.flatMap((run) => {
3
+ const given = (run.feedback ?? [])
4
+ .filter((item) => item.key === key)
5
+ .map((item) => item.score)
6
+ .filter(isNumber);
7
+ return given.length ? [given.reduce((sum, score) => sum + score, 0) / given.length] : [];
8
+ });
9
+ }
1
10
  export async function detectRegressions(options) {
2
11
  const read = async (window) => {
3
12
  const runs = await options.store.query({
@@ -27,6 +36,8 @@ export async function detectRegressions(options) {
27
36
  return regressions;
28
37
  }
29
38
  export function compareRuns(baseline, current, options = {}) {
39
+ if (options.confidence !== undefined)
40
+ return judgedRegressions(baseline, current, options, options.confidence);
30
41
  const name = options.group ?? 'runs';
31
42
  const minRuns = options.minRuns ?? 20;
32
43
  const metrics = new Set(options.metrics ?? ['error-rate', 'latency', 'cost']);
@@ -90,9 +101,7 @@ export function compareRuns(baseline, current, options = {}) {
90
101
  }
91
102
  }
92
103
  for (const feedbackKey of options.feedback ?? []) {
93
- const scores = (runs) => runs
94
- .flatMap((run) => (run.feedback ?? []).filter((item) => item.key === feedbackKey).map((item) => item.score))
95
- .filter(isNumber);
104
+ const scores = (runs) => runScores(runs, feedbackKey);
96
105
  const before = scores(baseline);
97
106
  const after = scores(current);
98
107
  if (before.length < minRuns || after.length < minRuns)
@@ -114,6 +123,123 @@ export function compareRuns(baseline, current, options = {}) {
114
123
  }
115
124
  return regressions;
116
125
  }
126
+ function judgedRegressions(baseline, current, options, confidence) {
127
+ if (!(confidence > 0 && confidence < 1))
128
+ throw new RangeError('confidence is between 0 and 1, such as 0.95');
129
+ const name = options.group ?? 'runs';
130
+ const minRuns = options.minRuns ?? 20;
131
+ const metrics = new Set(options.metrics ?? ['error-rate', 'latency', 'cost']);
132
+ const judge = (before, after, shape) => before.length >= minRuns && after.length >= minRuns
133
+ ? metricVerdict(before, after, {
134
+ ...shape,
135
+ confidence,
136
+ resamples: options.resamples ?? 1_000,
137
+ seed: options.seed ?? 1,
138
+ })
139
+ : undefined;
140
+ const regressions = [];
141
+ const add = (metric, verdict, counts, line) => {
142
+ if (!verdict?.regressed)
143
+ return;
144
+ regressions.push({
145
+ group: name,
146
+ metric,
147
+ baseline: verdict.baseline,
148
+ current: verdict.current,
149
+ change: verdict.change,
150
+ samples: counts,
151
+ interval: verdict.interval,
152
+ summary: `${name}: ${line} (${Math.round(confidence * 100)}% interval ${formatInterval(verdict.interval, metric)})`,
153
+ });
154
+ };
155
+ const counts = { baseline: baseline.length, current: current.length };
156
+ if (metrics.has('error-rate')) {
157
+ const errors = (runs) => runs.map((run) => (run.status === 'error' ? 1 : 0));
158
+ const verdict = judge(errors(baseline), errors(current), {
159
+ statistic: meanOf,
160
+ relative: false,
161
+ worse: 'rise',
162
+ minEffect: options.minErrorRateIncrease ?? 0.02,
163
+ });
164
+ add('error-rate', verdict, counts, `error rate rose from ${percent(verdict?.baseline ?? 0)} to ${percent(verdict?.current ?? 0)}`);
165
+ }
166
+ if (metrics.has('latency')) {
167
+ const latencies = (runs) => runs.map((run) => run.latencyMs).filter(isNumber);
168
+ const before = latencies(baseline);
169
+ const after = latencies(current);
170
+ const verdict = judge(before, after, {
171
+ statistic: (values) => percentile(values, 0.95) ?? 0,
172
+ relative: true,
173
+ worse: 'rise',
174
+ minEffect: options.latencyIncrease ?? 0.25,
175
+ });
176
+ const large = verdict && verdict.current - verdict.baseline >= (options.minLatencyChangeMs ?? 0);
177
+ add('latency-p95', large ? verdict : undefined, { baseline: before.length, current: after.length }, `p95 latency rose from ${Math.round(verdict?.baseline ?? 0)}ms to ${Math.round(verdict?.current ?? 0)}ms`);
178
+ }
179
+ if (metrics.has('cost')) {
180
+ const costs = (runs) => runs.map((run) => run.cost).filter(isNumber);
181
+ const before = costs(baseline);
182
+ const after = costs(current);
183
+ const verdict = judge(before, after, {
184
+ statistic: meanOf,
185
+ relative: true,
186
+ worse: 'rise',
187
+ minEffect: options.costIncrease ?? 0.25,
188
+ });
189
+ add('cost', verdict, { baseline: before.length, current: after.length }, `mean cost rose from $${(verdict?.baseline ?? 0).toFixed(4)} to $${(verdict?.current ?? 0).toFixed(4)}`);
190
+ }
191
+ for (const feedbackKey of options.feedback ?? []) {
192
+ const scores = (runs) => runScores(runs, feedbackKey);
193
+ const before = scores(baseline);
194
+ const after = scores(current);
195
+ const verdict = judge(before, after, {
196
+ statistic: meanOf,
197
+ relative: false,
198
+ worse: 'fall',
199
+ minEffect: options.minFeedbackDrop ?? 0.05,
200
+ });
201
+ add(`feedback:${feedbackKey}`, verdict, { baseline: before.length, current: after.length }, `${feedbackKey} fell from ${(verdict?.baseline ?? 0).toFixed(2)} to ${(verdict?.current ?? 0).toFixed(2)}`);
202
+ }
203
+ return regressions;
204
+ }
205
+ function metricVerdict(baseline, current, options) {
206
+ const change = (before, after) => options.relative ? (before === 0 ? 0 : after / before - 1) : after - before;
207
+ const before = options.statistic(baseline);
208
+ const after = options.statistic(current);
209
+ const random = seeded(options.seed);
210
+ const resample = (values) => Array.from({ length: values.length }, () => values[Math.floor(random() * values.length)]);
211
+ const draws = [];
212
+ for (let index = 0; index < options.resamples; index += 1) {
213
+ draws.push(change(options.statistic(resample(baseline)), options.statistic(resample(current))));
214
+ }
215
+ draws.sort((a, b) => a - b);
216
+ const tail = (1 - options.confidence) / 2;
217
+ const at = (quantile) => draws[Math.min(draws.length - 1, Math.max(0, Math.floor(quantile * draws.length)))];
218
+ const interval = [at(tail), at(1 - tail)];
219
+ const observed = change(before, after);
220
+ const regressed = options.worse === 'rise'
221
+ ? observed >= options.minEffect && interval[0] > 0
222
+ : -observed >= options.minEffect && interval[1] < 0;
223
+ return { baseline: before, current: after, change: observed, interval, regressed };
224
+ }
225
+ function seeded(seed) {
226
+ let state = seed >>> 0;
227
+ return () => {
228
+ state = (state + 0x6d2b79f5) >>> 0;
229
+ let value = state;
230
+ value = Math.imul(value ^ (value >>> 15), value | 1);
231
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
232
+ return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
233
+ };
234
+ }
235
+ function meanOf(values) {
236
+ return mean(values) ?? 0;
237
+ }
238
+ function formatInterval(interval, metric) {
239
+ const relative = metric === 'latency-p95' || metric === 'cost';
240
+ const show = (value) => (relative ? `${value >= 0 ? '+' : ''}${Math.round(value * 100)}%` : value.toFixed(3));
241
+ return `${show(interval[0])} to ${show(interval[1])}`;
242
+ }
117
243
  function isNumber(value) {
118
244
  return typeof value === 'number' && Number.isFinite(value);
119
245
  }
@@ -32,6 +32,8 @@ export async function* loadGitRepository(repository, options = {}) {
32
32
  const files = (await git(['ls-files', '-z'], directory, options)).split('\0').filter(Boolean).sort();
33
33
  const extensions = options.extensions && new Set(options.extensions.map((extension) => extension.toLowerCase()));
34
34
  const maxFileBytes = options.maxFileBytes ?? 1_000_000;
35
+ const { lstat, realpath } = await import('node:fs/promises');
36
+ const checkout = await realpath(directory);
35
37
  for (const file of files) {
36
38
  const extension = path.extname(file).toLowerCase();
37
39
  if (extensions && !extensions.has(extension))
@@ -40,7 +42,12 @@ export async function* loadGitRepository(repository, options = {}) {
40
42
  continue;
41
43
  let bytes;
42
44
  try {
43
- bytes = new Uint8Array(await readFile(path.join(directory, file)));
45
+ const full = path.join(directory, file);
46
+ if ((await lstat(full)).isSymbolicLink())
47
+ continue;
48
+ if (!(await realpath(full)).startsWith(checkout + path.sep))
49
+ continue;
50
+ bytes = new Uint8Array(await readFile(full));
44
51
  }
45
52
  catch {
46
53
  continue;
@@ -1,5 +1,6 @@
1
1
  import { parseUrl, safeFetch } from '../utils/safe-fetch.js';
2
2
  import { htmlToText } from './html.js';
3
+ const MAX_SITEMAP_BYTES = 50 * 1024 * 1024;
3
4
  export class WebLoaderError extends Error {
4
5
  url;
5
6
  status;
@@ -49,7 +50,12 @@ export async function sitemapUrls(url, options = {}) {
49
50
  let xml;
50
51
  if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
51
52
  const { gunzipSync } = await import('node:zlib');
52
- xml = gunzipSync(bytes).toString('utf8');
53
+ try {
54
+ xml = gunzipSync(bytes, { maxOutputLength: MAX_SITEMAP_BYTES }).toString('utf8');
55
+ }
56
+ catch (error) {
57
+ throw new WebLoaderError(`${sitemap} does not decompress to a sitemap of at most 50 MB: ${error instanceof Error ? error.message : String(error)}`, sitemap);
58
+ }
53
59
  }
54
60
  else
55
61
  xml = new TextDecoder().decode(bytes);
@@ -122,7 +122,21 @@ export interface StdioClientOptions {
122
122
  env?: Record<string, string>;
123
123
  /** Working directory for the process. */
124
124
  cwd?: string;
125
+ /**
126
+ * Which of this process's environment variables the server inherits, besides `env`. By default,
127
+ * only what a process needs to start: the path, the home and temporary directories, the user, the
128
+ * locale and terminal, the Windows system variables, proxies, and certificate settings. A server is
129
+ * often third-party code, and must not see this process's API keys or database URLs. A list adds
130
+ * the variables it names, `true` passes the whole environment, as releases before 2.4 did, and
131
+ * `false` passes nothing but `env`.
132
+ */
133
+ inheritEnv?: boolean | readonly string[];
125
134
  }
135
+ /**
136
+ * The environment a server process starts with. Names compare without case, as Windows compares
137
+ * them, and `LC_` locale settings are kept with the rest of the locale.
138
+ */
139
+ export declare function serverEnvironment(source: Record<string, string | undefined>, options?: Pick<StdioClientOptions, 'env' | 'inheritEnv'>): Record<string, string>;
126
140
  /**
127
141
  * Runs an MCP server as a child process and speaks to it over its stdin and stdout.
128
142
  *
@@ -131,6 +131,64 @@ export class McpClient {
131
131
  this.settle(id, undefined, error);
132
132
  }
133
133
  }
134
+ const INHERITED_ENV = new Set([
135
+ 'PATH',
136
+ 'PATHEXT',
137
+ 'HOME',
138
+ 'USER',
139
+ 'USERNAME',
140
+ 'LOGNAME',
141
+ 'SHELL',
142
+ 'TERM',
143
+ 'LANG',
144
+ 'LANGUAGE',
145
+ 'TZ',
146
+ 'TMPDIR',
147
+ 'TEMP',
148
+ 'TMP',
149
+ 'XDG_CONFIG_HOME',
150
+ 'XDG_CACHE_HOME',
151
+ 'XDG_DATA_HOME',
152
+ 'XDG_RUNTIME_DIR',
153
+ 'SYSTEMROOT',
154
+ 'SYSTEMDRIVE',
155
+ 'WINDIR',
156
+ 'COMSPEC',
157
+ 'APPDATA',
158
+ 'LOCALAPPDATA',
159
+ 'USERPROFILE',
160
+ 'HOMEDRIVE',
161
+ 'HOMEPATH',
162
+ 'PROGRAMDATA',
163
+ 'PROGRAMFILES',
164
+ 'PROGRAMFILES(X86)',
165
+ 'COMMONPROGRAMFILES',
166
+ 'PROCESSOR_ARCHITECTURE',
167
+ 'NUMBER_OF_PROCESSORS',
168
+ 'OS',
169
+ 'HTTP_PROXY',
170
+ 'HTTPS_PROXY',
171
+ 'NO_PROXY',
172
+ 'ALL_PROXY',
173
+ 'NODE_EXTRA_CA_CERTS',
174
+ 'SSL_CERT_FILE',
175
+ 'SSL_CERT_DIR',
176
+ ]);
177
+ export function serverEnvironment(source, options = {}) {
178
+ if (options.inheritEnv === false)
179
+ return { ...options.env };
180
+ const named = new Set((Array.isArray(options.inheritEnv) ? options.inheritEnv : []).map((name) => name.toUpperCase()));
181
+ const inherited = {};
182
+ for (const [name, value] of Object.entries(source)) {
183
+ if (value === undefined)
184
+ continue;
185
+ const upper = name.toUpperCase();
186
+ if (options.inheritEnv === true || INHERITED_ENV.has(upper) || upper.startsWith('LC_') || named.has(upper)) {
187
+ inherited[name] = value;
188
+ }
189
+ }
190
+ return { ...inherited, ...options.env };
191
+ }
134
192
  export function createStdioTransport(options) {
135
193
  let child;
136
194
  const decoder = new LineDecoder();
@@ -141,7 +199,7 @@ export function createStdioTransport(options) {
141
199
  const { spawn } = await import('node:child_process');
142
200
  const spawned = spawn(options.command, options.args ?? [], {
143
201
  cwd: options.cwd,
144
- env: { ...process.env, ...options.env },
202
+ env: serverEnvironment(process.env, options),
145
203
  stdio: ['pipe', 'pipe', 'inherit'],
146
204
  });
147
205
  spawned.stdout.setEncoding('utf8');
@@ -15,6 +15,12 @@ export interface McpServerConfig {
15
15
  env?: Record<string, string>;
16
16
  /** Working directory for the process. */
17
17
  cwd?: string;
18
+ /**
19
+ * Which of this process's own environment variables a local server inherits, besides `env`. By
20
+ * default, only what a process needs to start, never an API key; a list adds variables by name,
21
+ * `true` passes the whole environment, and `false` nothing but `env`.
22
+ */
23
+ inheritEnv?: boolean | string[];
18
24
  /** The endpoint of a remote server, reached over HTTP. */
19
25
  url?: string;
20
26
  /** Headers sent to a remote server, such as authorization. */
@@ -59,6 +59,7 @@ export class McpRegistry {
59
59
  ...(resolved.args ? { args: resolved.args } : {}),
60
60
  ...(resolved.env ? { env: resolved.env } : {}),
61
61
  ...(resolved.cwd ? { cwd: resolved.cwd } : {}),
62
+ ...(resolved.inheritEnv !== undefined ? { inheritEnv: resolved.inheritEnv } : {}),
62
63
  })
63
64
  : createHttpTransport({
64
65
  url: resolved.url,
@@ -163,6 +164,12 @@ export function validateMcpConfig(value) {
163
164
  throw new McpRegistryError(`Server "${name}": "${field}" is a list of strings`, name);
164
165
  }
165
166
  }
167
+ const inherit = server.inheritEnv;
168
+ if (inherit !== undefined &&
169
+ typeof inherit !== 'boolean' &&
170
+ !(Array.isArray(inherit) && inherit.every((item) => typeof item === 'string'))) {
171
+ throw new McpRegistryError(`Server "${name}": "inheritEnv" is true, false, or a list of names`, name);
172
+ }
166
173
  for (const field of ['env', 'headers']) {
167
174
  const map = server[field];
168
175
  if (map !== undefined &&
@@ -38,20 +38,24 @@ export class FilePromptStore {
38
38
  }
39
39
  async setLabel(label, expected) {
40
40
  const file = this.labelFile(label.name, label.label);
41
- if (expected !== undefined) {
42
- const current = await readJson(file);
43
- if (expected === null ? current !== undefined : current?.version !== expected)
44
- return false;
45
- }
46
- await writeJson(file, label);
47
- return true;
41
+ return serialized(file, async () => {
42
+ if (expected !== undefined) {
43
+ const current = await readJson(file);
44
+ if (expected === null ? current !== undefined : current?.version !== expected)
45
+ return false;
46
+ }
47
+ await writeJson(file, label);
48
+ return true;
49
+ });
48
50
  }
49
51
  async deleteLabel(name, label) {
50
52
  const file = this.labelFile(name, label);
51
- if ((await readJson(file)) === undefined)
52
- return false;
53
- await rm(file, { force: true });
54
- return true;
53
+ return serialized(file, async () => {
54
+ if ((await readJson(file)) === undefined)
55
+ return false;
56
+ await rm(file, { force: true });
57
+ return true;
58
+ });
55
59
  }
56
60
  async appendHistory(entry) {
57
61
  await mkdir(this.promptDir(entry.name), { recursive: true });
@@ -109,6 +113,19 @@ async function readJson(file) {
109
113
  return undefined;
110
114
  }
111
115
  }
116
+ const labelWrites = new Map();
117
+ async function serialized(file, work) {
118
+ const key = path.resolve(file);
119
+ const next = (labelWrites.get(key) ?? Promise.resolve()).catch(() => undefined).then(work);
120
+ labelWrites.set(key, next);
121
+ try {
122
+ return await next;
123
+ }
124
+ finally {
125
+ if (labelWrites.get(key) === next)
126
+ labelWrites.delete(key);
127
+ }
128
+ }
112
129
  async function writeJson(file, value) {
113
130
  await mkdir(path.dirname(file), { recursive: true });
114
131
  const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;