dd-trace 5.117.0 → 5.118.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 (83) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +2 -2
  45. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  48. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  49. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  50. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  51. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  52. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  53. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  54. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  55. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  57. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  58. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  59. package/packages/dd-trace/src/exporter.js +16 -9
  60. package/packages/dd-trace/src/llmobs/index.js +9 -1
  61. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  62. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  63. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  64. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  65. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  66. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  67. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  68. package/packages/dd-trace/src/llmobs/util.js +56 -3
  69. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  70. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  72. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  73. package/packages/dd-trace/src/plugins/index.js +1 -0
  74. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  75. package/packages/dd-trace/src/span_processor.js +1 -1
  76. package/packages/dd-trace/src/span_stats.js +22 -4
  77. package/ci/test-optimization-validation/command-suitability.js +0 -471
  78. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  79. package/ci/test-optimization-validation/init-probe.js +0 -246
  80. package/ci/test-optimization-validation/late-initialization.js +0 -63
  81. package/ci/test-optimization-validation/local-command.js +0 -163
  82. package/ci/test-optimization-validation/setup-runner.js +0 -97
  83. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -2,15 +2,22 @@
2
2
 
3
3
  /* eslint-disable no-console, eslint-rules/eslint-process-env */
4
4
 
5
+ const fs = require('node:fs')
5
6
  const path = require('path')
6
- const { spawn } = require('child_process')
7
+ const { spawn, spawnSync } = require('child_process')
7
8
 
8
- const { cleanupCommandOutputs, prepareCommandOutputs } = require('./command-output-policy')
9
9
  const {
10
- getExecutableForSpawn,
11
- isEnvExecutable,
12
- parseArgv,
13
- } = require('./executable')
10
+ cleanupCommandOutputs,
11
+ prepareCommandOutputs,
12
+ } = require('./command-output-policy')
13
+ const { getExecutableForSpawn } = require('./executable')
14
+ const {
15
+ environmentNamesEqual,
16
+ getEnvironmentValue,
17
+ isDatadogEnvironmentName,
18
+ mergeEnvironment,
19
+ setEnvironmentValue,
20
+ } = require('./environment')
14
21
  const { sanitizeConsoleText, sanitizeString } = require('./redaction')
15
22
  const { ensureSafeDirectory, writeFileSafely } = require('./safe-files')
16
23
 
@@ -78,6 +85,7 @@ const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024
78
85
  const EARLY_STOP_KILL_GRACE_MS = 500
79
86
  const TIMEOUT_KILL_GRACE_MS = 5000
80
87
  const TIMEOUT_FINALIZE_GRACE_MS = 1000
88
+ const WINDOWS_TASKKILL_TIMEOUT_MS = 10_000
81
89
 
82
90
  function runCommand (command, options = {}) {
83
91
  const {
@@ -110,8 +118,10 @@ function runCommand (command, options = {}) {
110
118
  durationMs: 0,
111
119
  timedOut: false,
112
120
  stdout: '',
121
+ stdoutOmittedBytes: 0,
113
122
  stdoutTruncated: false,
114
123
  stderr: '',
124
+ stderrOmittedBytes: 0,
115
125
  stderrTruncated: false,
116
126
  artifacts: {},
117
127
  }
@@ -122,51 +132,64 @@ function runCommand (command, options = {}) {
122
132
  } catch (error) {
123
133
  return Promise.reject(error)
124
134
  }
135
+ const missingRequiredEnvVars = getMissingRequiredEnvironmentNames(command.requiredEnvVars)
136
+ if (missingRequiredEnvVars.length > 0) {
137
+ result.missingRequiredEnvVars = missingRequiredEnvVars
138
+ result.stderr = 'The approved command requires environment variables that are not available: ' +
139
+ `${missingRequiredEnvVars.join(', ')}.\n`
140
+ result.durationMs = Date.now() - startedAt
141
+ return Promise.resolve(result)
142
+ }
125
143
  const outputStates = prepareCommandOutputs({ command, artifactRoot, outDir, repositoryRoot })
126
144
 
127
145
  return new Promise((resolve) => {
128
146
  let finalized = false
129
- let processGroupCleanupPending = false
147
+ let processTreeCleanupPending = false
130
148
  let pendingCloseResult
131
- const childEnv = {
132
- ...getBaseEnv(envMode),
133
- ...command.env,
134
- ...env,
135
- }
136
- if (command.env?.NODE_OPTIONS && env.NODE_OPTIONS) {
137
- childEnv.NODE_OPTIONS = mergeNodeOptions(env.NODE_OPTIONS, command.env.NODE_OPTIONS)
149
+ const childEnv = { ...getBaseEnv(envMode, command.requiredEnvVars) }
150
+ mergeEnvironment(childEnv, command.env)
151
+ mergeEnvironment(childEnv, env)
152
+ const commandNodeOptions = getEnvironmentValue(command.env || {}, 'NODE_OPTIONS')
153
+ const injectedNodeOptions = getEnvironmentValue(env, 'NODE_OPTIONS')
154
+ if (commandNodeOptions && injectedNodeOptions) {
155
+ setEnvironmentValue(childEnv, 'NODE_OPTIONS', mergeNodeOptions(injectedNodeOptions, commandNodeOptions))
138
156
  }
139
157
  for (const [name, value] of Object.entries(childEnv)) {
140
158
  if (value === undefined) delete childEnv[name]
141
159
  }
142
160
 
143
161
  const useProcessGroup = shouldUseProcessGroup()
162
+ const windowsTaskkillPath = getWindowsTaskkillPath()
163
+ if (process.platform === 'win32' && !windowsTaskkillPath) {
164
+ result.stderr = 'Validation cannot safely bound test processes because the Windows process-tree cleanup ' +
165
+ 'executable is unavailable.\n'
166
+ result.durationMs = Date.now() - startedAt
167
+ try {
168
+ finishCommandOutputCleanup(result, outputStates)
169
+ } catch (cleanupError) {
170
+ result.outputCleanupError = cleanupError?.message || String(cleanupError)
171
+ }
172
+ resolve(result)
173
+ return
174
+ }
175
+ const managesProcessTree = useProcessGroup || Boolean(windowsTaskkillPath)
144
176
  let child
145
177
  try {
146
178
  const executable = getExecutableForSpawn(command, { requireApproval: requireExecutableApproval })
147
179
  const argv0 = process.platform === 'win32' ? {} : { argv0: executable.argv0 }
148
- child = command.usesShell
149
- ? spawn(command.shellCommand, {
150
- ...argv0,
151
- cwd: command.cwd,
152
- detached: useProcessGroup,
153
- env: childEnv,
154
- shell: executable.path,
155
- stdio: ['ignore', 'pipe', 'pipe'],
156
- })
157
- : spawn(executable.path, command.argv.slice(1), {
158
- ...argv0,
159
- cwd: command.cwd,
160
- detached: useProcessGroup,
161
- env: childEnv,
162
- shell: false,
163
- stdio: ['ignore', 'pipe', 'pipe'],
164
- })
180
+ child = spawn(executable.path, command.argv.slice(1), {
181
+ ...argv0,
182
+ cwd: command.cwd,
183
+ detached: useProcessGroup,
184
+ env: childEnv,
185
+ shell: false,
186
+ stdio: ['ignore', 'pipe', 'pipe'],
187
+ })
165
188
  } catch (error) {
166
189
  result.stderr = `${error.stack || error}\n`
167
190
  result.durationMs = Date.now() - startedAt
168
191
  try {
169
- result.commandOutputPaths = cleanupCommandOutputs(outputStates)
192
+ finishCommandOutputCleanup(result, outputStates)
170
193
  } catch (cleanupError) {
171
194
  result.outputCleanupError = cleanupError?.message || String(cleanupError)
172
195
  }
@@ -183,13 +206,15 @@ function runCommand (command, options = {}) {
183
206
  let killTimer
184
207
  let finalizeTimer
185
208
  let stopTimer
209
+ let stdoutCapture
210
+ let stderrCapture
186
211
  const timeout = setTimeout(() => {
187
212
  result.timedOut = true
188
- processGroupCleanupPending = useProcessGroup
189
- signalChild(child, 'SIGTERM', useProcessGroup)
213
+ processTreeCleanupPending = managesProcessTree
214
+ signalChild(child, 'SIGTERM', useProcessGroup, windowsTaskkillPath)
190
215
  killTimer = setTimeout(() => {
191
- signalChild(child, 'SIGKILL', useProcessGroup)
192
- finishProcessGroupCleanup('SIGKILL')
216
+ signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
217
+ finishProcessTreeCleanup('SIGKILL')
193
218
  }, timeoutKillGraceMs)
194
219
  }, timeoutMs)
195
220
 
@@ -203,39 +228,45 @@ function runCommand (command, options = {}) {
203
228
 
204
229
  clearInterval(stopTimer)
205
230
  result.stoppedEarly = true
206
- processGroupCleanupPending = useProcessGroup
207
- signalChild(child, 'SIGTERM', useProcessGroup)
231
+ processTreeCleanupPending = managesProcessTree
232
+ signalChild(child, 'SIGTERM', useProcessGroup, windowsTaskkillPath)
208
233
  killTimer = setTimeout(() => {
209
- signalChild(child, 'SIGKILL', useProcessGroup)
210
- finishProcessGroupCleanup('SIGKILL')
234
+ signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
235
+ finishProcessTreeCleanup('SIGKILL')
211
236
  }, EARLY_STOP_KILL_GRACE_MS)
212
237
  }, 25)
213
238
  }
214
239
 
215
240
  child.stdout.on('data', chunk => {
216
- const capture = appendCapturedOutput(result.stdout, chunk, maxOutputBytes)
241
+ const capture = appendCapturedOutput(stdoutCapture, chunk, maxOutputBytes)
242
+ stdoutCapture = capture
217
243
  result.stdout = capture.output
218
- result.stdoutTruncated = result.stdoutTruncated || capture.truncated
244
+ result.stdoutOmittedBytes = capture.omittedBytes
245
+ result.stdoutTruncated = capture.truncated
219
246
  })
220
247
  child.stderr.on('data', chunk => {
221
- const capture = appendCapturedOutput(result.stderr, chunk, maxOutputBytes)
248
+ const capture = appendCapturedOutput(stderrCapture, chunk, maxOutputBytes)
249
+ stderrCapture = capture
222
250
  result.stderr = capture.output
223
- result.stderrTruncated = result.stderrTruncated || capture.truncated
251
+ result.stderrOmittedBytes = capture.omittedBytes
252
+ result.stderrTruncated = capture.truncated
224
253
  })
225
254
  child.on('error', err => {
226
255
  result.stderr += `${err.stack || err}\n`
227
256
  finalize(null, null)
228
257
  })
229
258
  child.on('close', (code, signal) => {
230
- if (processGroupCleanupPending) {
259
+ if (processTreeCleanupPending) {
231
260
  pendingCloseResult = { code, signal }
261
+ signalChild(child, 'SIGKILL', useProcessGroup, windowsTaskkillPath)
262
+ finishProcessTreeCleanup(signal)
232
263
  return
233
264
  }
234
265
  finalize(code, signal)
235
266
  })
236
267
 
237
- function finishProcessGroupCleanup (fallbackSignal) {
238
- processGroupCleanupPending = false
268
+ function finishProcessTreeCleanup (fallbackSignal) {
269
+ processTreeCleanupPending = false
239
270
  if (pendingCloseResult) {
240
271
  finalize(pendingCloseResult.code, pendingCloseResult.signal || fallbackSignal)
241
272
  return
@@ -255,7 +286,7 @@ function runCommand (command, options = {}) {
255
286
  result.durationMs = Date.now() - startedAt
256
287
 
257
288
  try {
258
- result.commandOutputPaths = cleanupCommandOutputs(outputStates)
289
+ finishCommandOutputCleanup(result, outputStates)
259
290
  } catch (err) {
260
291
  result.outputCleanupError = err && err.message ? err.message : String(err)
261
292
  result.stderr += '\n[test-optimization-validator] could not clean up command outputs: ' +
@@ -271,13 +302,13 @@ function runCommand (command, options = {}) {
271
302
  writeFileSafely(
272
303
  artifactRoot,
273
304
  result.artifacts.stdout,
274
- sanitizeString(formatCapturedOutput(result.stdout, result.stdoutTruncated, maxOutputBytes)),
305
+ sanitizeString(result.stdout),
275
306
  'command stdout artifact'
276
307
  )
277
308
  writeFileSafely(
278
309
  artifactRoot,
279
310
  result.artifacts.stderr,
280
- sanitizeString(formatCapturedOutput(result.stderr, result.stderrTruncated, maxOutputBytes)),
311
+ sanitizeString(result.stderr),
281
312
  'command stderr artifact'
282
313
  )
283
314
  writeFileSafely(artifactRoot, result.artifacts.command, `${JSON.stringify({
@@ -291,7 +322,9 @@ function runCommand (command, options = {}) {
291
322
  timedOut: result.timedOut,
292
323
  stoppedEarly: result.stoppedEarly,
293
324
  stdoutTruncated: result.stdoutTruncated,
325
+ stdoutOmittedBytes: result.stdoutOmittedBytes,
294
326
  stderrTruncated: result.stderrTruncated,
327
+ stderrOmittedBytes: result.stderrOmittedBytes,
295
328
  maxOutputBytes,
296
329
  commandOutputPaths: result.commandOutputPaths,
297
330
  outputCleanupError: result.outputCleanupError,
@@ -308,6 +341,17 @@ function runCommand (command, options = {}) {
308
341
  })
309
342
  }
310
343
 
344
+ /**
345
+ * Cleans command outputs after the command exits.
346
+ *
347
+ * @param {object} result command result
348
+ * @param {object[]} outputStates prepared command output state
349
+ * @returns {void}
350
+ */
351
+ function finishCommandOutputCleanup (result, outputStates) {
352
+ result.commandOutputPaths = cleanupCommandOutputs(outputStates)
353
+ }
354
+
311
355
  /**
312
356
  * Returns the effective bounded execution settings used for one project command.
313
357
  *
@@ -327,48 +371,92 @@ function getCommandExecutionSettings (command) {
327
371
  /**
328
372
  * Appends output while retaining only the latest bytes for diagnostic artifacts.
329
373
  *
330
- * @param {string} current currently captured output
374
+ * @param {object|undefined} current currently captured output state
331
375
  * @param {Buffer|string} chunk new output chunk
332
376
  * @param {number} maxBytes maximum retained bytes
333
377
  * @returns {{output: string, truncated: boolean}} retained output and truncation flag
334
378
  */
335
379
  function appendCapturedOutput (current, chunk, maxBytes) {
336
- const next = Buffer.concat([
337
- Buffer.from(current),
338
- Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)),
339
- ])
340
-
341
- if (next.length <= maxBytes) {
342
- return {
343
- output: next.toString('utf8'),
344
- truncated: false,
380
+ const nextChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
381
+ const totalBytes = (current?.totalBytes || 0) + nextChunk.length
382
+ const headLimit = Math.floor(maxBytes / 2)
383
+ const tailLimit = maxBytes - headLimit
384
+ let head = current?.head || Buffer.alloc(0)
385
+ let tail
386
+
387
+ if (current?.truncated) {
388
+ const combinedTail = Buffer.concat([current.tail, nextChunk])
389
+ tail = combinedTail.subarray(Math.max(0, combinedTail.length - tailLimit))
390
+ } else {
391
+ const combined = Buffer.concat([current?.tail || Buffer.alloc(0), nextChunk])
392
+ if (combined.length <= maxBytes) {
393
+ return {
394
+ head,
395
+ tail: combined,
396
+ totalBytes,
397
+ omittedBytes: 0,
398
+ output: combined.toString('utf8'),
399
+ truncated: false,
400
+ }
345
401
  }
402
+ head = combined.subarray(0, headLimit)
403
+ tail = combined.subarray(combined.length - tailLimit)
346
404
  }
347
405
 
406
+ const omittedBytes = totalBytes - head.length - tail.length
348
407
  return {
349
- output: next.subarray(next.length - maxBytes).toString('utf8'),
408
+ head,
409
+ tail,
410
+ totalBytes,
411
+ omittedBytes,
412
+ output: `${head.toString('utf8')}\n[test-optimization-validator] ${omittedBytes} bytes omitted\n` +
413
+ tail.toString('utf8'),
350
414
  truncated: true,
351
415
  }
352
416
  }
353
417
 
418
+ function shouldUseProcessGroup () {
419
+ return process.platform !== 'win32'
420
+ }
421
+
354
422
  /**
355
- * Adds truncation context to a captured command output artifact.
423
+ * Resolves the fixed Windows process-tree cleanup executable without PATH lookup.
356
424
  *
357
- * @param {string} output captured output
358
- * @param {boolean} truncated whether earlier output was omitted
359
- * @param {number} maxBytes maximum retained bytes
360
- * @returns {string} output artifact content
425
+ * @returns {string|undefined} physical taskkill executable
361
426
  */
362
- function formatCapturedOutput (output, truncated, maxBytes) {
363
- if (!truncated) return output
364
- return `[test-optimization-validator] output truncated to last ${maxBytes} bytes\n${output}`
365
- }
427
+ function getWindowsTaskkillPath () {
428
+ if (process.platform !== 'win32') return
366
429
 
367
- function shouldUseProcessGroup () {
368
- return process.platform !== 'win32'
430
+ const systemRoot = getEnvironmentValue(process.env, 'SystemRoot') ||
431
+ getEnvironmentValue(process.env, 'WINDIR')
432
+ if (!systemRoot || !path.win32.isAbsolute(systemRoot)) return
433
+
434
+ try {
435
+ const root = fs.realpathSync(systemRoot)
436
+ const candidate = path.win32.join(root, 'System32', 'taskkill.exe')
437
+ const stat = fs.lstatSync(candidate)
438
+ const physical = fs.realpathSync(candidate)
439
+ const relative = path.win32.relative(root, physical)
440
+ if (!stat.isFile() || stat.isSymbolicLink() ||
441
+ relative.startsWith('..') || path.win32.isAbsolute(relative)) return
442
+ return physical
443
+ } catch {}
369
444
  }
370
445
 
371
- function signalChild (child, signal, useProcessGroup) {
446
+ function signalChild (child, signal, useProcessGroup, windowsTaskkillPath) {
447
+ if (windowsTaskkillPath) {
448
+ try {
449
+ const args = ['/PID', String(child.pid), '/T', '/F']
450
+ const outcome = spawnSync(windowsTaskkillPath, args, {
451
+ shell: false,
452
+ stdio: 'ignore',
453
+ timeout: WINDOWS_TASKKILL_TIMEOUT_MS,
454
+ windowsHide: true,
455
+ })
456
+ if (!outcome.error && outcome.status === 0) return
457
+ } catch {}
458
+ }
459
+
372
460
  try {
373
461
  if (useProcessGroup) {
374
462
  process.kill(-child.pid, signal)
@@ -379,16 +467,29 @@ function signalChild (child, signal, useProcessGroup) {
379
467
  child.kill(signal)
380
468
  }
381
469
 
382
- function getBaseEnv (envMode) {
470
+ function getBaseEnv (envMode, requiredEnvVars = []) {
383
471
  if (envMode !== 'clean') return process.env
384
472
 
385
473
  const cleanEnv = {}
386
- for (const name of CLEAN_ENV_ALLOWLIST) {
387
- if (process.env[name] !== undefined) cleanEnv[name] = process.env[name]
474
+ for (const name of [...CLEAN_ENV_ALLOWLIST, ...requiredEnvVars]) {
475
+ const entry = Object.entries(process.env).find(([candidate]) => {
476
+ return environmentNamesEqual(candidate, name)
477
+ })
478
+ if (entry) setEnvironmentValue(cleanEnv, name, entry[1])
388
479
  }
389
480
  return cleanEnv
390
481
  }
391
482
 
483
+ /**
484
+ * Finds explicitly approved ambient variables that are unavailable at execution time.
485
+ *
486
+ * @param {string[]|undefined} requiredEnvVars approved variable names
487
+ * @returns {string[]} missing names
488
+ */
489
+ function getMissingRequiredEnvironmentNames (requiredEnvVars = []) {
490
+ return requiredEnvVars.filter(name => getEnvironmentValue(process.env, name) === undefined)
491
+ }
492
+
392
493
  function buildDatadogEnv ({ fixture, outputRoot, scenario, framework }) {
393
494
  const offline = buildOfflineValidationEnv({ fixture, outputRoot })
394
495
  return {
@@ -405,7 +506,7 @@ function buildDatadogEnv ({ fixture, outputRoot, scenario, framework }) {
405
506
  }
406
507
  }
407
508
 
408
- function buildCiWiringEnv ({ fixture, outputRoot }) {
509
+ function buildOfflineCaptureEnv ({ fixture, outputRoot }) {
409
510
  return {
410
511
  ...buildOfflineValidationEnv({ fixture, outputRoot }),
411
512
  [VALIDATION_CAPTURE_MODE_ENV]: 'sample',
@@ -454,138 +555,15 @@ function buildOfflineValidationEnv ({ fixture, outputRoot }) {
454
555
  */
455
556
  function assertNoInlineValidationEnvOverrides (command, env) {
456
557
  if (!env[VALIDATION_MODE_ENV]) return
457
- const reservedEnvNames = new Set([
458
- ...VALIDATION_RESERVED_ENV_NAMES,
459
- ...Object.keys(env).filter(name => {
460
- return name.startsWith('DD_') || name.startsWith('_DD_') || name.startsWith('OTEL_')
461
- }),
462
- ])
463
-
464
- if (command.usesShell) {
465
- rejectReservedShellAssignments(command.shellCommand, reservedEnvNames)
466
- return
467
- }
468
-
469
- const parsed = parseArgv(command.argv)
470
- rejectReservedEnvSplitStrings(command.argv, reservedEnvNames)
471
- if (parsed.ignoreEnvironment) throwEnvironmentReset()
472
- if (parsed.unsupportedEnvOption) throwUnsupportedEnvOption(parsed.unsupportedEnvOption)
473
- for (const name of Object.keys(parsed.prefixEnv)) {
474
- if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
475
- }
476
- for (const name of parsed.unsetEnvNames) {
477
- if (reservedEnvNames.has(name)) throwReservedEnvOverride(name)
478
- }
479
-
480
- if (isPosixShellExecutable(command.argv[parsed.commandIndex])) {
481
- for (let index = parsed.commandIndex + 1; index < command.argv.length - 1; index++) {
482
- const value = command.argv[index]
483
- if (isShellCommandFlag(value) && typeof command.argv[index + 1] === 'string') {
484
- rejectReservedShellAssignments(command.argv[index + 1], reservedEnvNames)
485
- }
486
- }
487
- }
488
- }
489
-
490
- /**
491
- * Rejects reserved variable assignments and removals in shell source.
492
- *
493
- * @param {string} shellCommand shell source
494
- * @param {Set<string>} reservedEnvNames validator-controlled environment names
495
- */
496
- function rejectReservedShellAssignments (shellCommand, reservedEnvNames) {
497
- const source = String(shellCommand || '')
498
- const environmentReset =
499
- /\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-(?=\s|$)|-i\b|--ignore-environment\b)/i
500
-
501
- if (environmentReset.test(source)) throwEnvironmentReset()
502
-
503
- for (const name of reservedEnvNames) {
504
- const escapedName = escapeRegExp(name)
505
- const assignment = new RegExp(
506
- String.raw`(?:^|[\s;&|()'"])(?:export\s+|set\s+)?(?:\$env:)?${escapedName}\s*\+?=`,
507
- 'i'
508
- )
509
- const removal = new RegExp(
510
- String.raw`(?:\bunset(?:\s+(?:-[A-Za-z]+|[A-Za-z_][A-Za-z0-9_]*))*\s+|` +
511
- String.raw`\benv(?:\.exe)?\s+(?:(?![;&|()]).)*?(?:-u\s*|--unset(?:=|\s+))|` +
512
- String.raw`\bRemove-Item\s+(?:[^;&|]*\s)?env:)${escapedName}\b`,
513
- 'i'
514
- )
515
-
516
- if (assignment.test(source) || removal.test(source)) throwReservedEnvOverride(name)
517
- }
518
- }
519
-
520
- /**
521
- * Rejects reserved environment changes hidden inside env --split-string arguments.
522
- *
523
- * @param {string[]} argv structured command arguments
524
- * @param {Set<string>} reservedEnvNames validator-controlled environment names
525
- * @returns {void}
526
- */
527
- function rejectReservedEnvSplitStrings (argv, reservedEnvNames) {
528
- if (!Array.isArray(argv) || !isEnvExecutable(argv[0])) return
529
-
530
- for (let index = 1; index < argv.length; index++) {
531
- const argument = argv[index]
532
- if (argument === '-S' || argument === '--split-string') {
533
- if (typeof argv[index + 1] === 'string') {
534
- rejectReservedShellAssignments(`env ${argv[index + 1]}`, reservedEnvNames)
535
- }
536
- index++
537
- continue
558
+ for (const name of Object.keys(command.env || {})) {
559
+ const normalized = process.platform === 'win32' ? name.toUpperCase() : name
560
+ if (VALIDATION_RESERVED_ENV_NAMES.some(reserved => environmentNamesEqual(reserved, name)) ||
561
+ isDatadogEnvironmentName(name) || normalized.startsWith('OTEL_')) {
562
+ throw new Error(`Direct-runner adapter must not override validator-controlled environment variable ${name}.`)
538
563
  }
539
-
540
- const splitString = /^(?:-S|--split-string=)(.+)$/.exec(argument)?.[1]
541
- if (splitString !== undefined) rejectReservedShellAssignments(`env ${splitString}`, reservedEnvNames)
542
564
  }
543
565
  }
544
566
 
545
- function isShellCommandFlag (value) {
546
- return /^-[A-Za-z]*c[A-Za-z]*$/.test(value)
547
- }
548
-
549
- function isPosixShellExecutable (value) {
550
- return /^(?:a|ba|da|k|z)?sh$/.test(path.basename(String(value || '')).toLowerCase())
551
- }
552
-
553
- /**
554
- * Throws a customer-facing error for unsafe inline validation environment changes.
555
- *
556
- * @param {string} name reserved environment variable
557
- */
558
- function throwReservedEnvOverride (name) {
559
- throw new Error(
560
- `Refusing inline ${name} changes during live validation because they can bypass the offline validation mode. ` +
561
- 'Record CI-provided values in command.env so the validator can apply its private diagnostic settings.'
562
- )
563
- }
564
-
565
- function throwEnvironmentReset () {
566
- throw new Error(
567
- 'Refusing to clear the command environment during live validation because this would remove the offline ' +
568
- 'validation and Datadog initialization settings.'
569
- )
570
- }
571
-
572
- function throwUnsupportedEnvOption (option) {
573
- throw new Error(
574
- `Refusing unsupported env option ${option} during live validation because its environment effects cannot be ` +
575
- 'verified safely.'
576
- )
577
- }
578
-
579
- /**
580
- * Escapes a literal for use in a regular expression.
581
- *
582
- * @param {string} value literal value
583
- * @returns {string} escaped value
584
- */
585
- function escapeRegExp (value) {
586
- return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
587
- }
588
-
589
567
  function withCiPreloads (nodeOptions = '', framework) {
590
568
  let result = nodeOptions.trim()
591
569
 
@@ -621,7 +599,7 @@ function formatNodeRequire (filename) {
621
599
  }
622
600
 
623
601
  function serializeCommand (command) {
624
- return command.usesShell ? command.shellCommand : command.argv.join(' ')
602
+ return command.argv.join(' ')
625
603
  }
626
604
 
627
605
  /**
@@ -631,7 +609,6 @@ function serializeCommand (command) {
631
609
  * @returns {string} unambiguous customer-facing command
632
610
  */
633
611
  function serializeApprovalCommand (command) {
634
- if (command.usesShell) return command.shellCommand
635
612
  return command.argv.map(formatApprovalArgument).join(' ')
636
613
  }
637
614
 
@@ -649,52 +626,19 @@ function formatApprovalArgument (value) {
649
626
  }
650
627
 
651
628
  function serializeDisplayCommand (command) {
652
- if (typeof command.displayCommand === 'string' && command.displayCommand.trim()) {
653
- return command.displayCommand.trim()
654
- }
655
-
656
- if (command.usesShell) return command.shellCommand
657
-
658
- return getDisplayArgv(command.argv).join(' ')
629
+ return command.argv.join(' ')
659
630
  }
660
631
 
661
632
  function getCommandDetails (command) {
662
- if (command.usesShell) return
663
-
664
- const details = getDisplayDetails(command.argv)
665
- if (!details.exactCommandCollapsed) return
666
-
667
- return details
668
- }
669
-
670
- function getDisplayArgv (argv) {
671
- const { prefixAssignments, commandIndex, corepackIndex } = parseArgv(argv)
672
- if (corepackIndex !== -1) return [...prefixAssignments, ...argv.slice(corepackIndex + 1)]
673
- return [...prefixAssignments, ...argv.slice(commandIndex)]
674
- }
675
-
676
- function getDisplayDetails (argv) {
677
- const { commandIndex, corepackIndex, pathAdjusted } = parseArgv(argv)
678
- const displayArgv = getDisplayArgv(argv)
679
- const details = {
680
- exactCommandCollapsed: displayArgv.join(' ') !== argv.join(' '),
681
- }
682
-
683
- if (pathAdjusted) details.pathAdjusted = true
684
-
685
- if (corepackIndex !== -1) {
686
- details.runtimeWrapper = 'node/corepack'
687
- details.packageManager = argv[corepackIndex + 1]
688
- } else if (commandIndex > 0) {
689
- details.runtimeWrapper = 'env'
633
+ return {
634
+ executionBoundary: 'validator-owned-direct-runner',
635
+ runner: command.argv[1],
690
636
  }
691
-
692
- return details
693
637
  }
694
638
 
695
639
  module.exports = {
696
640
  runCommand,
697
- buildCiWiringEnv,
641
+ buildOfflineCaptureEnv,
698
642
  buildDatadogEnv,
699
643
  getBaseEnv,
700
644
  getCommandDetails,