@goodandready/dsh-moa 0.2.47 → 0.2.49

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.
package/lib/history.js CHANGED
@@ -51,6 +51,14 @@ export function invalidateHistoryCache() {
51
51
  _historyCache = null
52
52
  }
53
53
 
54
+ function getOrInitCache(filePath, stat) {
55
+ if (!_historyCache || _historyCache.filePath !== filePath || _historyCache.mtimeMs !== stat.mtimeMs || _historyCache.size !== stat.size) {
56
+ _historyCache = { filePath, mtimeMs: stat.mtimeMs, size: stat.size, leaderboards: new Map(), runsById: new Map(), totalCount: null }
57
+ }
58
+ if (!_historyCache.runsById) _historyCache.runsById = new Map()
59
+ return _historyCache
60
+ }
61
+
54
62
  /**
55
63
  * Checks if the history file exceeds the size threshold and rotates it.
56
64
  * Moves current file to `${filePath}.1`, removing any older `.1` backup.
@@ -277,22 +285,8 @@ export function countFileLinesSync(filePath, stat) {
277
285
  try { fs.closeSync(fd) } catch { /* best effort */ }
278
286
  }
279
287
 
280
- if (
281
- !_historyCache ||
282
- _historyCache.filePath !== filePath ||
283
- _historyCache.mtimeMs !== stat.mtimeMs ||
284
- _historyCache.size !== stat.size
285
- ) {
286
- _historyCache = {
287
- filePath,
288
- mtimeMs: stat.mtimeMs,
289
- size: stat.size,
290
- leaderboards: new Map(),
291
- totalCount: count,
292
- }
293
- } else {
294
- _historyCache.totalCount = count
295
- }
288
+ const cache = getOrInitCache(filePath, stat)
289
+ cache.totalCount = count
296
290
  return count
297
291
  }
298
292
 
@@ -314,29 +308,17 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
314
308
  const lines = raw.trim().split('\n').filter(Boolean)
315
309
  const total = lines.length
316
310
 
317
- if (
318
- !_historyCache ||
319
- _historyCache.filePath !== filePath ||
320
- _historyCache.mtimeMs !== stat.mtimeMs ||
321
- _historyCache.size !== stat.size
322
- ) {
323
- _historyCache = {
324
- filePath,
325
- mtimeMs: stat.mtimeMs,
326
- size: stat.size,
327
- leaderboards: new Map(),
328
- totalCount: total,
329
- }
330
- } else {
331
- _historyCache.totalCount = total
332
- }
311
+ const cache = getOrInitCache(filePath, stat)
312
+ cache.totalCount = total
333
313
 
334
314
  const sliced = lines
335
315
  .slice(Math.max(0, total - offset - limit), total - offset)
336
316
  .reverse()
337
317
  .map((line) => {
338
318
  try {
339
- return JSON.parse(line)
319
+ const item = JSON.parse(line)
320
+ if (item?.id) cache.runsById.set(item.id, item)
321
+ return item
340
322
  } catch {
341
323
  return null
342
324
  }
@@ -348,24 +330,22 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
348
330
 
349
331
  // For larger files: read only tail lines backwards to avoid splitting 10MB in memory
350
332
  const tailLines = readTailLinesSync(filePath, needed)
333
+ const cache = getOrInitCache(filePath, stat)
351
334
  const pageLines = tailLines.slice(offset, offset + limit)
352
335
  const runs = pageLines
353
336
  .map((line) => {
354
337
  try {
355
- return JSON.parse(line)
338
+ const item = JSON.parse(line)
339
+ if (item?.id) cache.runsById.set(item.id, item)
340
+ return item
356
341
  } catch {
357
342
  return null
358
343
  }
359
344
  })
360
345
  .filter(Boolean)
361
346
 
362
- let total = (_historyCache &&
363
- _historyCache.filePath === filePath &&
364
- _historyCache.mtimeMs === stat.mtimeMs &&
365
- _historyCache.size === stat.size &&
366
- typeof _historyCache.totalCount === 'number')
367
- ? _historyCache.totalCount
368
- : undefined
347
+ const isCached = _historyCache && _historyCache.filePath === filePath && _historyCache.mtimeMs === stat.mtimeMs && _historyCache.size === stat.size
348
+ let total = isCached && typeof _historyCache.totalCount === 'number' ? _historyCache.totalCount : undefined
369
349
  if (typeof total !== 'number') {
370
350
  total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
371
351
  }
@@ -392,24 +372,9 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
392
372
  const presetFilter = typeof options === 'string' ? options : (options?.presetFilter || options?.preset || null)
393
373
  const filterKey = presetFilter || 'all'
394
374
 
395
- if (
396
- _historyCache &&
397
- _historyCache.filePath === filePath &&
398
- _historyCache.mtimeMs === stat.mtimeMs &&
399
- _historyCache.size === stat.size
400
- ) {
401
- if (_historyCache.leaderboards.has(filterKey)) {
402
- return _historyCache.leaderboards.get(filterKey)
403
- }
404
- } else {
405
- _historyCache = {
406
- filePath,
407
- mtimeMs: stat.mtimeMs,
408
- size: stat.size,
409
- leaderboards: new Map(),
410
- totalCount: null,
411
- }
412
- }
375
+ const isCached = _historyCache && _historyCache.filePath === filePath && _historyCache.mtimeMs === stat.mtimeMs && _historyCache.size === stat.size
376
+ if (isCached && _historyCache.leaderboards.has(filterKey)) return _historyCache.leaderboards.get(filterKey)
377
+ const cache = getOrInitCache(filePath, stat)
413
378
 
414
379
  const raw = fs.readFileSync(filePath, 'utf8')
415
380
  const lines = raw.trim().split('\n').filter(Boolean)
@@ -500,18 +465,51 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
500
465
  }
501
466
 
502
467
  /**
503
- * Retrieves a single history run by ID.
468
+ * Retrieves a single history run by ID using in-memory cache and tail reading (#235).
504
469
  */
505
470
  export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
506
471
  try {
507
- if (!fs.existsSync(filePath)) return null
472
+ if (!runId || !fs.existsSync(filePath)) return null
473
+ const stat = fs.statSync(filePath)
474
+ const cache = getOrInitCache(filePath, stat)
475
+ if (cache.runsById.has(runId)) return cache.runsById.get(runId)
476
+
477
+ if (stat.size <= 65536) {
478
+ const raw = fs.readFileSync(filePath, 'utf8')
479
+ const lines = raw.trim().split('\n').filter(Boolean)
480
+ for (let i = lines.length - 1; i >= 0; i--) {
481
+ try {
482
+ const item = JSON.parse(lines[i])
483
+ if (item?.id) {
484
+ cache.runsById.set(item.id, item)
485
+ if (item.id === runId) return item
486
+ }
487
+ } catch { /* skip corrupt */ }
488
+ }
489
+ return null
490
+ }
491
+
492
+ const tailLines = readTailLinesSync(filePath, 100)
493
+ for (const line of tailLines) {
494
+ try {
495
+ const item = JSON.parse(line)
496
+ if (item?.id) {
497
+ cache.runsById.set(item.id, item)
498
+ if (item.id === runId) return item
499
+ }
500
+ } catch { /* skip corrupt */ }
501
+ }
502
+
508
503
  const raw = fs.readFileSync(filePath, 'utf8')
509
504
  const lines = raw.trim().split('\n').filter(Boolean)
510
505
  for (let i = lines.length - 1; i >= 0; i--) {
511
506
  try {
512
507
  const item = JSON.parse(lines[i])
513
- if (item && item.id === runId) return item
514
- } catch { /* corrupt line skipped */ }
508
+ if (item?.id) {
509
+ cache.runsById.set(item.id, item)
510
+ if (item.id === runId) return item
511
+ }
512
+ } catch { /* skip corrupt */ }
515
513
  }
516
514
  return null
517
515
  } catch {
package/lib/moa-budget.js CHANGED
@@ -87,11 +87,31 @@ export function estimatePanelCost(judges = [], numCandidates = 1, prices = {}, m
87
87
  * Calculates accurate synthesis prompt tokens accounting for full user prompt,
88
88
  * judge criteria, and reference candidate texts (#158).
89
89
  */
90
- export function calcSynthesisPromptTokens(userPrompt = '', referenceOutputs = [], judgeCriteria = '', overhead = 2600) {
91
- const promptChars = (typeof userPrompt === 'string' ? userPrompt.length : 0) +
90
+ export function calcSynthesisPromptTokens(userPrompt = '', referenceOutputs = [], judgeCriteria = '', overhead = 2600, options = {}) {
91
+ const opts = (typeof overhead === 'object' && overhead !== null) ? overhead : (options || {})
92
+ const baseOverhead = typeof overhead === 'number' ? overhead : 2600
93
+ let promptChars = (typeof userPrompt === 'string' ? userPrompt.length : 0) +
92
94
  (typeof judgeCriteria === 'string' ? judgeCriteria.length : 0) +
93
- (Array.isArray(referenceOutputs) ? referenceOutputs.reduce((acc, r) => acc + (r.text?.length || 0), 0) : 0) +
94
- overhead
95
+ baseOverhead
96
+
97
+ if (Array.isArray(referenceOutputs)) {
98
+ for (const r of referenceOutputs) {
99
+ promptChars += (r.text?.length || 0)
100
+ if (r.testResult) {
101
+ promptChars += 150 + (r.testResult.output?.length || 0)
102
+ }
103
+ if (r.files && r.files.length > 0) {
104
+ promptChars += 50 + r.files.reduce((acc, f) => acc + (f.relativePath?.length || 0), 0)
105
+ }
106
+ }
107
+ }
108
+ if (opts.priorTurnBaseline) promptChars += opts.priorTurnBaseline.length + 50
109
+ if (opts.consensusReport) {
110
+ const crText = typeof opts.consensusReport === 'string' ? opts.consensusReport : JSON.stringify(opts.consensusReport)
111
+ promptChars += crText.length + 50
112
+ }
113
+ if (opts.compositeMergeDirective) promptChars += opts.compositeMergeDirective.length + 50
114
+
95
115
  return Math.max(50, Math.ceil(promptChars / 4))
96
116
  }
97
117
 
package/lib/moa-runner.js CHANGED
@@ -48,7 +48,7 @@ export async function runMoAPipeline({
48
48
  const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
49
49
  const maxTokens = typeof preset?.max_tokens === 'number' ? preset.max_tokens : 4096
50
50
  const judgeCriteria = preset?.judge_criteria || ''
51
- const isFastMode = referenceModels.length === 1 && !preset?.curator_synthesis
51
+ let isFastMode = referenceModels.length === 1 && !preset?.curator_synthesis
52
52
  const isCuratorSynthesis = Boolean(preset?.curator_synthesis), isStreamAggregator = preset?.stream_aggregator !== false
53
53
  const isQuorumEnabled = Boolean(preset?.quorum_enabled), gracePeriodSec = typeof preset?.grace_period_sec === 'number' ? preset.grace_period_sec : 10
54
54
  const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
@@ -93,6 +93,9 @@ export async function runMoAPipeline({
93
93
  return createBudgetAbortPayload({ reason: budgetCheck.reason, primaryJudge, referenceOutputs: [], currentSpentUsd: 0, preset, isRefinement, isFastMode, collectedCtx, startTime })
94
94
  }
95
95
  const effectiveReferences = budgetCheck.references
96
+ if (effectiveReferences.length === 1 && !preset?.curator_synthesis) {
97
+ isFastMode = true
98
+ }
96
99
  if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
97
100
  onProgress(`✂️ *${budgetCheck.reason}*\n`)
98
101
  }
@@ -401,7 +404,15 @@ export async function runMoAPipeline({
401
404
  : null
402
405
 
403
406
  // 8. Synthesis phase via primary judge or fallback chain
404
- const synthPromptTokens = calcSynthesisPromptTokens(userPrompt, successfulRefs, judgeCriteria)
407
+ const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
408
+ curatorSynthesis: isCuratorSynthesis,
409
+ blindEvaluation: isBlindEvaluation,
410
+ priorTurnBaseline,
411
+ compositeMergeDirective,
412
+ consensusReport: multiJudgeResult?.consensusReport,
413
+ })
414
+ const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
415
+
405
416
  if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0 && !isFastMode) {
406
417
  const currentSpent = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + (multiJudgeResult?.totalCostUsd || 0)
407
418
  const downNext = estimateDownstreamCost(primaryJudge, [], successfulRefs.length, prices, maxTokens, synthPromptTokens)
@@ -411,13 +422,6 @@ export async function runMoAPipeline({
411
422
  return createBudgetAbortPayload({ reason: phaseCheck.reason, primaryJudge, referenceOutputs, currentSpentUsd: currentSpent, preset, isRefinement, isFastMode, collectedCtx, startTime, multiJudgeResult })
412
423
  }
413
424
  }
414
- const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
415
- curatorSynthesis: isCuratorSynthesis,
416
- blindEvaluation: isBlindEvaluation,
417
- priorTurnBaseline,
418
- compositeMergeDirective,
419
- consensusReport: multiJudgeResult?.consensusReport,
420
- })
421
425
  let synthesizedText = ''
422
426
  let aggUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
423
427
  let chosenJudge = primaryJudge
@@ -43,34 +43,47 @@ export function resolveCandidateDir(baseDir, candidateIndex, options = {}) {
43
43
  }
44
44
  }
45
45
 
46
- const COMPUTED_SHELL_EVASION = /(?:base64\s+(?:-d|--decode|-D)|\beval\s+["\']?\$|\bxxd\s+-r|\bprintf\s+[^|\n]+\|\s*(?:ba|z|da)?sh|\bopenssl\s+enc\s+-d)/i
46
+ export function decodeEscapedStrings(str) {
47
+ if (!str || typeof str !== 'string') return ''
48
+ let res = 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
+ return res.replace(/(?<![A-Za-z0-9+/=])(?:(?:[A-Za-z0-9+/]{4}){2,}|(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))(?![A-Za-z0-9+/=])/g, (m) => {
52
+ try {
53
+ const dec = Buffer.from(m, 'base64').toString('utf8')
54
+ return /^[\x20-\x7E\r\n\t]+$/.test(dec) ? dec : m
55
+ } catch (err) {
56
+ logger.debug('[dsh-moa:testGate] base64 decode skip:', err?.message || err)
57
+ return m
58
+ }
59
+ })
60
+ }
61
+
62
+ 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)/i
47
63
 
48
- const UNIX_ROOT_PREFIX = /^\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\/(?:user|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\/(?:[a-zA-Z0-9_\-.]+)|Applications|Library|System|Volumes|private)(?:\/|$)/
64
+ 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)(?:\/|$)/
49
65
 
50
66
  /**
51
67
  * Validates that an executed script or command does not reference files outside allowed roots (#161, #225).
52
68
  */
53
69
  function scanPathTokensForLeak(text, allowedRoots, executionDir) {
54
70
  if (!text || typeof text !== 'string') return null
55
- const sanitized = text
56
- .replace(/https?:\/\/[^\s"'<>]+/gi, '')
57
- .replace(/\b(?:node|file|npm|data):[^\s"'<>]+/gi, '')
58
- const matches = sanitized.match(/(?:[a-zA-Z]:[\\/][^\s"'<>]+|(?<![\w.@\/\\])\/(?:[a-zA-Z0-9_\-.]+\/)*[a-zA-Z0-9_\-.]+|(?<![\w.@\/\\])\.\.[\\/][^\s"'<>]*)/g) || []
59
- for (const raw of matches) {
60
- if (/^\/(usr\/(bin|lib|include)|bin|lib|lib64|etc|dev)(\/|$)/.test(raw) && !/\.(txt|json|env|npmrc|key|ssh|md)/i.test(raw)) {
61
- continue
62
- }
63
- const isAbs = path.isAbsolute(raw)
64
- const isUp = raw.startsWith('..')
65
- if (!isAbs && !isUp) continue
66
- if (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(raw)) continue
67
- const resolved = path.resolve(executionDir, raw)
68
- const allowed = allowedRoots.some((r) => r && (resolved === r || resolved.startsWith(r + path.sep)))
69
- if (!allowed) {
70
- return raw
71
+ const checkText = (t) => {
72
+ const sanitized = t.replace(/https?:\/\/[^\s"'<>]+/gi, '').replace(/\b(?:node|file|npm|data):[^\s"'<>]+/gi, '')
73
+ const matches = sanitized.match(/(?:[a-zA-Z]:[\\/][^\s"'<>]+|(?<![\w.@\/\\])\/(?:[a-zA-Z0-9_\-.]+\/)*[a-zA-Z0-9_\-.]+|(?<![\w.@\/\\])\.\.[\\/][^\s"'<>]*)/g) || []
74
+ for (const raw of matches) {
75
+ if (/^\/(usr\/(bin|lib|include)|bin|lib|lib64|etc|dev)(\/|$)/.test(raw) && !/\.(txt|json|env|npmrc|key|ssh|md)/i.test(raw)) continue
76
+ const isAbs = path.isAbsolute(raw)
77
+ if ((!isAbs && !raw.startsWith('..')) || (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(raw))) continue
78
+ const resolved = path.resolve(executionDir, raw)
79
+ if (!allowedRoots.some((r) => r && (resolved === r || resolved.startsWith(r + path.sep)))) return raw
71
80
  }
81
+ return null
72
82
  }
73
- return null
83
+ const rawLeak = checkText(text)
84
+ if (rawLeak) return rawLeak
85
+ const decoded = decodeEscapedStrings(text)
86
+ return decoded !== text ? checkText(decoded) : null
74
87
  }
75
88
 
76
89
  /**
@@ -288,8 +301,17 @@ export async function runCandidateTestGate({
288
301
  const cp = require('node:child_process');
289
302
  const path = require('node:path');
290
303
  const allowed = ${JSON.stringify(allowedReads)};
291
- const COMPUTED_SHELL_EVASION = /(?:base64\\s+(?:-d|--decode|-D)|\\beval\\s+["\']?\\$|\\bxxd\\s+-r|\\bprintf\\s+[^|\\n]+\\|\\s*(?:ba|z|da)?sh|\\bopenssl\\s+enc\\s+-d)/i;
292
- const UNIX_ROOT_PREFIX = /^\\/(?:home|root|etc|var|tmp|opt|mnt|srv|media|proc|sys|dev|usr|bin|lib|lib64|run\\/(?:user|systemd|lock|credentials|secrets|udev|initramfs|mount|shm|dbus|sshd|motd)|sbin|boot|Users\\/(?:[a-zA-Z0-9_\\-.]+)|Applications|Library|System|Volumes|private)(?:\\/|$)/;
304
+ function decodeEscaped(str) {
305
+ if (!str || typeof str !== 'string') return '';
306
+ const norm = str.split(/\\+/).join(String.fromCharCode(92));
307
+ return norm.replace(new RegExp(String.fromCharCode(92, 92) + '(?:([0-7]{1,3})|x([0-9a-fA-F]{2}))', 'g'), (m, oct, hex) => {
308
+ if (oct) return String.fromCharCode(parseInt(oct, 8));
309
+ if (hex) return String.fromCharCode(parseInt(hex, 16));
310
+ return m;
311
+ });
312
+ }
313
+ 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)/i;
314
+ 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)(?:\\/|$)/;
293
315
  function checkSinglePath(p) {
294
316
  if (!p || typeof p !== 'string') return true;
295
317
  if (/^\\/(usr\\/(bin|lib|include)|bin|lib|lib64|etc|dev)(\\/|$)/.test(p) && !/\\.(txt|json|env|npmrc|key|ssh|md)/i.test(p)) return true;
@@ -303,10 +325,18 @@ function checkSinglePath(p) {
303
325
  function scanStringForPaths(str) {
304
326
  if (!str || typeof str !== 'string') return true;
305
327
  if (!checkSinglePath(str)) return false;
328
+ const dec = decodeEscaped(str);
329
+ if (dec !== str && !checkSinglePath(dec)) return false;
306
330
  const matches = str.match(/(?:[a-zA-Z]:[\\\\/][^\\s"'<>]+|(?<![\\w.@/\\\\])\\/(?:[a-zA-Z0-9_\\-.]+\\/)*[a-zA-Z0-9_\\-.]+|(?<![\\w.@/\\\\])\\.\\.[\\\\/][^\\s"'<>]*)/g) || [];
307
331
  for (const m of matches) {
308
332
  if (!checkSinglePath(m)) return false;
309
333
  }
334
+ if (dec !== str) {
335
+ const decMatches = dec.match(/(?:[a-zA-Z]:[\\\\/][^\\s"'<>]+|(?<![\\w.@/\\\\])\\/(?:[a-zA-Z0-9_\\-.]+\\/)*[a-zA-Z0-9_\\-.]+|(?<![\\w.@/\\\\])\\.\\.[\\\\/][^\\s"'<>]*)/g) || [];
336
+ for (const m of decMatches) {
337
+ if (!checkSinglePath(m)) return false;
338
+ }
339
+ }
310
340
  return true;
311
341
  }
312
342
  function guardArgs(cmd, args) {
package/lib/routes.js CHANGED
@@ -512,7 +512,6 @@ ctx.effect(() => {
512
512
  writeJson(res, 429, { ok: false, error: 'Rate limit exceeded: please wait before starting another run' })
513
513
  return
514
514
  }
515
- _lastRunTimestamp = now
516
515
 
517
516
  try {
518
517
  const raw = await readBody(req)
@@ -522,6 +521,7 @@ ctx.effect(() => {
522
521
  writeJson(res, 400, { ok: false, error: 'Empty prompt' })
523
522
  return
524
523
  }
524
+ _lastRunTimestamp = now
525
525
 
526
526
  const cfg = live()
527
527
  if (cfg.enabled === false) {
package/lib/updater.js CHANGED
@@ -41,10 +41,11 @@ export function isTrustedUpdateRequest(request) {
41
41
  }
42
42
  }
43
43
 
44
- /** General safe write check for plugin write endpoints (CSRF / Cross-Site protection) */
44
+ /** General safe write check for plugin write endpoints (CSRF / Cross-Site protection, #234) */
45
45
  export function isSafeWriteRequest(request) {
46
46
  const remote = request?.socket?.remoteAddress || request?.connection?.remoteAddress;
47
47
  const isLoop = !remote || isLoopback(remote);
48
+ if (!isLoop) return false;
48
49
 
49
50
  const site = header(request, 'sec-fetch-site');
50
51
  if (site === 'cross-site') return false;
@@ -52,13 +53,6 @@ export function isSafeWriteRequest(request) {
52
53
  const origin = header(request, 'origin');
53
54
  const host = header(request, 'host');
54
55
 
55
- // If non-loopback caller, require valid same-origin browser proof
56
- if (!isLoop) {
57
- if (!origin || !host) return false;
58
- if (site && site !== 'same-origin' && site !== 'none') return false;
59
- }
60
-
61
- // Same-site without matching origin is rejected regardless of remote
62
56
  if (site === 'same-site') {
63
57
  if (!origin || !host) return false;
64
58
  }
@@ -68,9 +62,7 @@ export function isSafeWriteRequest(request) {
68
62
  try {
69
63
  const url = new URL(origin);
70
64
  if (host && url.host !== host) return false;
71
- if (!isLoop) {
72
- if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
73
- }
65
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
74
66
  } catch {
75
67
  return false;
76
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",