@goodandready/dsh-moa 0.2.55 → 0.2.57

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.
@@ -1,3 +1,11 @@
1
+ /**
2
+ * DeepSeek Harness Mixture of Agents (MoA) — Test Execution Gate (#161, #242, #260)
3
+ *
4
+ * Runs candidate code through verification tests in an isolated sandbox.
5
+ * Enforces Node.js permission model containment, fence interception,
6
+ * and rejection of child process evasion without OS-level isolation.
7
+ */
8
+
1
9
  import os from 'node:os'
2
10
  import fs from 'node:fs/promises'
3
11
  import fsSync from 'node:fs'
@@ -6,31 +14,20 @@ import { spawn } from 'node:child_process'
6
14
  import { resolveCandidateFolder } from './file-workspace.js'
7
15
  import { bestEffort } from './best-effort.js'
8
16
  import { logger } from './logger.js'
17
+ import {
18
+ isSandboxEvasion,
19
+ isOsSandboxAvailable,
20
+ resolveTestCommand,
21
+ scanPathTokensForLeak,
22
+ getScrubbedEnv,
23
+ buildPermOpts,
24
+ generateFenceCode,
25
+ } from './moa-sandbox.js'
26
+
27
+ export { getScrubbedEnv, scanPathTokensForLeak }
9
28
 
10
29
  const MAX_CAPTURE_BYTES = 256 * 1024
11
30
 
12
- /**
13
- * Returns a sanitized environment stripping sensitive tokens and secrets (#161).
14
- */
15
- export function getScrubbedEnv(overrides = {}) {
16
- const safeVars = new Set([
17
- 'PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TMPDIR', 'TEMP', 'TMP',
18
- 'USER', 'LOGNAME', 'HOME', 'SHELL', 'TERM', 'NODE_ENV', 'SYSTEMROOT',
19
- 'WINDIR', 'COMSPEC', 'PATHEXT',
20
- ])
21
- const scrubbed = {}
22
- for (const [key, val] of Object.entries(process.env)) {
23
- if (!val || key.startsWith('MOA_')) continue
24
- if (/(API_KEY|TOKEN|SECRET|PASSWORD|PASSWD|AUTH|CREDENTIAL|PRIVATE|KEY|ACCESS)/i.test(key)) {
25
- continue
26
- }
27
- if (safeVars.has(key) || key.startsWith('LC_')) {
28
- scrubbed[key] = val
29
- }
30
- }
31
- return { ...scrubbed, ...overrides }
32
- }
33
-
34
31
  /**
35
32
  * Resolves the directory for candidate sandbox.
36
33
  */
@@ -43,46 +40,6 @@ export function resolveCandidateDir(baseDir, candidateIndex, options = {}) {
43
40
  }
44
41
  }
45
42
 
46
- export function decodeEscapedStrings(str) {
47
- if (!str || typeof str !== 'string') return ''
48
- return str.replace(/\\+/g, '\\').replace(/\\(?:([0-7]{1,3})|x([0-9a-fA-F]{2}))/g, (m, oct, hex) =>
49
- oct ? String.fromCharCode(parseInt(oct, 8)) : hex ? String.fromCharCode(parseInt(hex, 16)) : m
50
- )
51
- }
52
-
53
- const COMPUTED_SHELL_EVASION = /(?:base64\s+(?:-d|--decode|-D)[^|\n;]*\|\s*(?:ba|z|da)?sh|\beval\s+["']?\$|\bxxd\s+-r[^|\n;]*\|\s*(?:ba|z|da)?sh|\bprintf\s+[^|\n]+\|\s*(?:ba|z|da)?sh|\bopenssl\s+enc\s+-d[^|\n;]*\|\s*(?:ba|z|da)?sh|\bprintf\s+(?:%s|["']%s["'])\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\$\(\s*(?:printf|echo)\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\x60\s*(?:printf|echo)\s+.*["'](?:\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\b(?:cat|head|tail|source|\.|touch|rm|cp|mv)\s+["']?\$[A-Za-z_]|>\s*["']?\$[A-Za-z_]|>>\s*["']?\$[A-Za-z_])/i
54
-
55
- const UNIX_ROOT_PREFIX = /^\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\/(?:user\/\d+|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\/(?:[a-zA-Z0-9_\-.]+)|Applications|Library|System|Volumes|private)(?:\/|$)/
56
-
57
- /**
58
- * Validates that an executed script or command does not reference files outside allowed roots (#161, #225).
59
- */
60
- function scanPathTokensForLeak(text, allowedRoots, executionDir) {
61
- if (!text || typeof text !== 'string') return null
62
- const checkText = (t) => {
63
- const sanitized = t.replace(/https?:\/\/[^\s"'<>]+/gi, '').replace(/\b(?:node|file|npm|data):[^\s"'<>]+/gi, '')
64
- const matches = sanitized.match(/(?:[a-zA-Z]:[\\/][^\s"'<>]+|(?<![\w.@\/\\])\/(?:[a-zA-Z0-9_\-.]+\/)*[a-zA-Z0-9_\-.]+|(?<![\w.@\/\\])\.\.[\\/][^\s"'<>]*)/g) || []
65
- for (const raw of matches) {
66
- if (/^\/(usr\/(bin|lib|include)|bin|lib|lib64|etc|dev)(\/|$)/.test(raw) && !/\.(txt|json|env|npmrc|key|ssh|md)/i.test(raw)) continue
67
- const isAbs = path.isAbsolute(raw)
68
- if ((!isAbs && !raw.startsWith('..')) || (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(raw))) continue
69
- const resolved = path.resolve(executionDir, raw)
70
- if (!allowedRoots.some((r) => r && (resolved === r || resolved.startsWith(r + path.sep)))) return raw
71
- }
72
- return null
73
- }
74
- const rawLeak = checkText(text)
75
- if (rawLeak) return rawLeak
76
- const segmented = text.match(/\bprintf\s+(?:%s|['"]%s['"])\s+((?:['"][^'"]*['"]\s*)+)/i)
77
- if (segmented) {
78
- const assembled = (segmented[1].match(/['"]([^'"]*)['"]/g) || []).map((s) => s.slice(1, -1)).join('')
79
- const assembledLeak = checkText(assembled)
80
- if (assembledLeak) return assembledLeak
81
- }
82
- const decoded = decodeEscapedStrings(text)
83
- return decoded !== text ? checkText(decoded) : null
84
- }
85
-
86
43
  /**
87
44
  * Executes a test command in a candidate sandbox or ephemeral test staging directory.
88
45
  */
@@ -142,10 +99,10 @@ export async function runCandidateTestGate({
142
99
  return res
143
100
  }
144
101
 
145
- let resolvedCmd = trimmedCmd
146
- resolvedCmd = replacePlaceholderSafely(resolvedCmd, '{candidateDir}', candidateDir)
147
- resolvedCmd = replacePlaceholderSafely(resolvedCmd, '{baseDir}', cwd)
148
- resolvedCmd = replacePlaceholderSafely(resolvedCmd, '{stageDir}', executionDir)
102
+ let resolvedCmd = resolveTestCommand({ testCommand: trimmedCmd, executionDir })
103
+ resolvedCmd = replacePlaceholderSafely(resolvedCmd, '<CANDIDATE_DIR>', executionDir)
104
+ resolvedCmd = replacePlaceholderSafely(resolvedCmd, '{candidateDir}', executionDir)
105
+ resolvedCmd = replacePlaceholderSafely(resolvedCmd, '$CANDIDATE_DIR', executionDir)
149
106
 
150
107
  let exitCode = 0
151
108
  let stdout = ''
@@ -163,7 +120,9 @@ export async function runCandidateTestGate({
163
120
  stderr = customRes?.stderr || ''
164
121
  } else {
165
122
  const allowedRoots = [executionDir, cwd, ...(candidateDir && candidateDir !== executionDir ? [candidateDir] : [])]
166
- if (COMPUTED_SHELL_EVASION.test(resolvedCmd)) {
123
+
124
+ const cmdEvasion = isSandboxEvasion(resolvedCmd)
125
+ if (cmdEvasion) {
167
126
  const durationMs = Date.now() - startTime
168
127
  return {
169
128
  candidateIndex,
@@ -171,7 +130,7 @@ export async function runCandidateTestGate({
171
130
  exitCode: 1,
172
131
  durationMs,
173
132
  command: resolvedCmd,
174
- output: 'Access denied: test command contains obfuscated or computed command execution',
133
+ output: `Access denied: test command contains ${cmdEvasion}`,
175
134
  summary: `FAIL (code 1, ${durationMs}ms)`,
176
135
  }
177
136
  }
@@ -201,7 +160,7 @@ export async function runCandidateTestGate({
201
160
  }
202
161
  }
203
162
 
204
- // 2. Scan script files referenced by command or present in candidate workspace (#161)
163
+ // 2. Scan script files referenced by command or present in candidate workspace (#161, #260)
205
164
  const candFiles = fsSync.existsSync(executionDir) ? fsSync.readdirSync(executionDir, { recursive: true }) : []
206
165
  const scriptTokens = new Set([
207
166
  ...cmdTokens.map((t) => t.replace(/^["']|["']$/g, '')),
@@ -213,7 +172,8 @@ export async function runCandidateTestGate({
213
172
  if (fsSync.existsSync(scriptPath) && !fsSync.statSync(scriptPath).isDirectory()) {
214
173
  try {
215
174
  const scriptText = fsSync.readFileSync(scriptPath, 'utf8')
216
- if (COMPUTED_SHELL_EVASION.test(scriptText)) {
175
+ const scriptEvasion = isSandboxEvasion(scriptText)
176
+ if (scriptEvasion) {
217
177
  const durationMs = Date.now() - startTime
218
178
  return {
219
179
  candidateIndex,
@@ -221,7 +181,7 @@ export async function runCandidateTestGate({
221
181
  exitCode: 1,
222
182
  durationMs,
223
183
  command: resolvedCmd,
224
- output: 'Access denied: test script contains obfuscated or computed command execution',
184
+ output: `Access denied: test script contains ${scriptEvasion}`,
225
185
  summary: `FAIL (code 1, ${durationMs}ms)`,
226
186
  }
227
187
  }
@@ -244,13 +204,14 @@ export async function runCandidateTestGate({
244
204
  }
245
205
  }
246
206
 
247
- // 3. Scan package.json scripts if package.json exists in executionDir (#161, #206)
207
+ // 3. Scan package.json scripts if package.json exists in executionDir (#161, #206, #260)
248
208
  const candPkgFile = path.join(executionDir, 'package.json')
249
209
  if (fsSync.existsSync(candPkgFile)) {
250
210
  try {
251
211
  const pkgObj = JSON.parse(fsSync.readFileSync(candPkgFile, 'utf8'))
252
212
  const scriptsContent = Object.values(pkgObj.scripts || {}).join('\n')
253
- if (COMPUTED_SHELL_EVASION.test(scriptsContent)) {
213
+ const pkgEvasion = isSandboxEvasion(scriptsContent)
214
+ if (pkgEvasion) {
254
215
  const durationMs = Date.now() - startTime
255
216
  return {
256
217
  candidateIndex,
@@ -258,7 +219,7 @@ export async function runCandidateTestGate({
258
219
  exitCode: 1,
259
220
  durationMs,
260
221
  command: resolvedCmd,
261
- output: 'Access denied: package.json test script contains obfuscated or computed command execution',
222
+ output: `Access denied: package.json test script contains ${pkgEvasion}`,
262
223
  summary: `FAIL (code 1, ${durationMs}ms)`,
263
224
  }
264
225
  }
@@ -304,90 +265,20 @@ export async function runCandidateTestGate({
304
265
  : ['/usr', '/lib', '/lib64', '/etc']),
305
266
  ].filter(Boolean)
306
267
 
307
- // Create security fence module for child process containment (#161, #206)
268
+ // Create security fence module for child process containment (#161, #206, #260)
269
+ const hasOsSandbox = isOsSandboxAvailable()
308
270
  fenceFile = path.join(executionDir, `.moa-fence-${Date.now()}.cjs`)
309
- const fenceCode = `
310
- const cp = require('node:child_process');
311
- const path = require('node:path');
312
- const allowed = ${JSON.stringify(allowedReads)};
313
- function decodeEscaped(str) {
314
- if (!str || typeof str !== 'string') return '';
315
- const norm = str.split(/\\+/).join(String.fromCharCode(92));
316
- return norm.replace(new RegExp(String.fromCharCode(92, 92) + '(?:([0-7]{1,3})|x([0-9a-fA-F]{2}))', 'g'), (m, oct, hex) => {
317
- if (oct) return String.fromCharCode(parseInt(oct, 8));
318
- if (hex) return String.fromCharCode(parseInt(hex, 16));
319
- return m;
320
- });
321
- }
322
- const COMPUTED_SHELL_EVASION = /(?:base64\\s+(?:-d|--decode|-D)[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\beval\\s+["']?\\$|\\bxxd\\s+-r[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\bprintf\\s+[^|\\n]+\\|\\s*(?:ba|z|da)?sh|\\bopenssl\\s+enc\\s+-d[^|\\n;]*\\|\\s*(?:ba|z|da)?sh|\\bprintf\\s+(?:%s|["']%s["'])\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\$\\(\\s*(?:printf|echo)\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\x60\\s*(?:printf|echo)\\s+.*["'](?:\\/|mnt|home|tmp|etc|var|usr|root|opt)["']|\\b(?:cat|head|tail|source|\\.|touch|rm|cp|mv)\\s+["']?\\$[A-Za-z_]|>\\s*["']?\\$[A-Za-z_]|>>\\s*["']?\\$[A-Za-z_])/i;
323
- const UNIX_ROOT_PREFIX = /^\\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\\/(?:user\\/\\d+|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\\/(?:[a-zA-Z0-9_\\-.]+)|Applications|Library|System|Volumes|private)(?:\\/|$)/;
324
- function checkSinglePath(p) {
325
- if (!p || typeof p !== 'string') return true;
326
- if (/^\\/(usr\\/(bin|lib|include)|bin|lib|lib64|etc|dev)(\\/|$)/.test(p) && !/\\.(txt|json|env|npmrc|key|ssh|md)/i.test(p)) return true;
327
- const isAbs = path.isAbsolute(p);
328
- const isUp = p.startsWith('..');
329
- if (!isAbs && !isUp) return true;
330
- if (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(p)) return true;
331
- const res = path.resolve(process.cwd(), p);
332
- return allowed.some(root => root && (res === root || res.startsWith(root + path.sep)));
333
- }
334
- function scanStringForPaths(str) {
335
- if (!str || typeof str !== 'string') return true;
336
- if (!checkSinglePath(str)) return false;
337
- const dec = decodeEscaped(str);
338
- if (dec !== str && !checkSinglePath(dec)) return false;
339
- const matches = str.match(/(?:[a-zA-Z]:[\\\\/][^\\s"'<>]+|(?<![\\w.@/\\\\])\\/(?:[a-zA-Z0-9_\\-.]+\\/)*[a-zA-Z0-9_\\-.]+|(?<![\\w.@/\\\\])\\.\\.[\\\\/][^\\s"'<>]*)/g) || [];
340
- for (const m of matches) {
341
- if (!checkSinglePath(m)) return false;
342
- }
343
- if (dec !== str) {
344
- const decMatches = dec.match(/(?:[a-zA-Z]:[\\\\/][^\\s"'<>]+|(?<![\\w.@/\\\\])\\/(?:[a-zA-Z0-9_\\-.]+\\/)*[a-zA-Z0-9_\\-.]+|(?<![\\w.@/\\\\])\\.\\.[\\\\/][^\\s"'<>]*)/g) || [];
345
- for (const m of decMatches) {
346
- if (!checkSinglePath(m)) return false;
347
- }
348
- }
349
- return true;
350
- }
351
- function guardArgs(cmd, args) {
352
- const full = [cmd, ...(Array.isArray(args) ? args : [])].join(' ');
353
- if (COMPUTED_SHELL_EVASION.test(full)) {
354
- throw new Error('ERR_ACCESS_DENIED: child process computed execution pattern blocked: ' + full);
355
- }
356
- if (cmd && !scanStringForPaths(String(cmd))) {
357
- throw new Error('ERR_ACCESS_DENIED: child process command outside sandbox: ' + cmd);
358
- }
359
- if (Array.isArray(args)) {
360
- for (const a of args) {
361
- if (a && !scanStringForPaths(String(a))) {
362
- throw new Error('ERR_ACCESS_DENIED: child process argument outside sandbox: ' + a);
363
- }
364
- }
365
- }
366
- }
367
- const origSpawn = cp.spawn; cp.spawn = function(cmd, args, opts) { guardArgs(cmd, args); return origSpawn.apply(this, arguments); };
368
- const origSpawnSync = cp.spawnSync; cp.spawnSync = function(cmd, args, opts) { guardArgs(cmd, args); return origSpawnSync.apply(this, arguments); };
369
- const origExecFile = cp.execFile; cp.execFile = function(file, args, opts, cb) { guardArgs(file, args); return origExecFile.apply(this, arguments); };
370
- const origExecFileSync = cp.execFileSync; cp.execFileSync = function(file, args, opts) { guardArgs(file, args); return origExecFileSync.apply(this, arguments); };
371
- const origExec = cp.exec; cp.exec = function(cmd, opts, cb) { guardArgs(cmd, [cmd]); return origExec.apply(this, arguments); };
372
- const origExecSync = cp.execSync; cp.execSync = function(cmd, opts) { guardArgs(cmd, [cmd]); return origExecSync.apply(this, arguments); };
373
- const fsMod = require('node:fs');
374
- function guardFs(p, isWrite) {
375
- if (!p || typeof p !== 'string') return;
376
- const isAbs = path.isAbsolute(p);
377
- const isUp = p.startsWith('..');
378
- if (!isAbs && !isUp) return;
379
- const resolved = path.resolve(process.cwd(), p);
380
- const checkRoots = isWrite ? [process.cwd(), process.env.HOME].filter(Boolean) : allowed;
381
- const ok = checkRoots.some(r => r && (resolved === r || resolved.startsWith(r + path.sep)));
382
- if (!ok) throw new Error('ERR_ACCESS_DENIED: fs access outside sandbox: ' + p);
383
- }
384
- const origRF = fsMod.readFileSync; fsMod.readFileSync = function(p) { guardFs(p, false); return origRF.apply(this, arguments); };
385
- const origWF = fsMod.writeFileSync; fsMod.writeFileSync = function(p) { guardFs(p, true); return origWF.apply(this, arguments); };
386
- const origAF = fsMod.appendFileSync; fsMod.appendFileSync = function(p) { guardFs(p, true); return origAF.apply(this, arguments); };
387
- `
271
+ const fenceCode = generateFenceCode({ allowedReads, hasOsSandbox })
388
272
  fsSync.writeFileSync(fenceFile, fenceCode, 'utf8')
389
273
 
390
- const permOpts = `--permission ${allowedReads.map((p) => `--allow-fs-read=${p}`).join(' ')} --allow-fs-write=${executionDir} --allow-fs-write=${effectiveHome} --allow-child-process --require ${fenceFile}`
274
+ const permOpts = buildPermOpts({
275
+ allowedReads,
276
+ executionDir,
277
+ effectiveHome,
278
+ fenceFile,
279
+ hasOsSandbox,
280
+ })
281
+
391
282
  const nodePath = path.join(cwd, 'node_modules')
392
283
  const env = getScrubbedEnv({
393
284
  HOME: effectiveHome,