@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 +61 -63
- package/lib/moa-budget.js +24 -4
- package/lib/moa-runner.js +13 -9
- package/lib/moa-test-gate.js +51 -21
- package/lib/routes.js +1 -1
- package/lib/updater.js +3 -11
- package/package.json +1 -1
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
514
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/lib/moa-test-gate.js
CHANGED
|
@@ -43,34 +43,47 @@ export function resolveCandidateDir(baseDir, candidateIndex, options = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
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
|
|
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
|
|
56
|
-
.replace(/https?:\/\/[^\s"'<>]+/gi, '')
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
continue
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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 (
|
|
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
|
}
|