@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.
- package/README.md +3 -2
- package/README.ru.md +2 -1
- package/README.zh.md +4 -0
- package/lib/client.js +495 -0
- package/lib/index.js +9 -0
- package/lib/moa-prompts.js +2 -0
- package/lib/moa-runner.js +19 -14
- package/lib/moa-sandbox.js +287 -0
- package/lib/moa-schema.js +1 -0
- package/lib/moa-test-gate.js +46 -155
- package/lib/moa-workbench.js +481 -0
- package/lib/routes.js +17 -3
- package/package.json +1 -1
package/lib/moa-runner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { executeMultiJudgePanel, buildCompositeBlockDirectives, runMultiJudgePha
|
|
|
12
12
|
import { applyBudgetGuardrails, checkPhaseBudget, estimateRound2Cost, estimateDownstreamCost, calcSynthesisPromptTokens, createBudgetAbortPayload } from './moa-budget.js'
|
|
13
13
|
import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
|
|
14
14
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
15
|
+
import { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
|
|
15
16
|
// Re-exports for consumers & backward compatibility
|
|
16
17
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
17
18
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
@@ -19,6 +20,7 @@ export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
|
19
20
|
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
20
21
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
21
22
|
export { streamMoATurn, streamPromoteTurn } from './moa-stream.js'
|
|
23
|
+
export { formatFileScoresDirective, parseFileScores } from './moa-workbench.js'
|
|
22
24
|
/**
|
|
23
25
|
* Executes the full Mixture of Agents pipeline.
|
|
24
26
|
*/
|
|
@@ -403,12 +405,18 @@ export async function runMoAPipeline({
|
|
|
403
405
|
? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
|
|
404
406
|
: null
|
|
405
407
|
|
|
408
|
+
// 7e. Interactive Workbench File Scoring Directive
|
|
409
|
+
const fileScoringDirective = (preset?.interactive_workbench_enabled)
|
|
410
|
+
? formatFileScoresDirective()
|
|
411
|
+
: null
|
|
412
|
+
|
|
406
413
|
// 8. Synthesis phase via primary judge or fallback chain
|
|
407
414
|
const synthesisPrompt = buildSynthesisPrompt(userPrompt, referenceOutputs, judgeCriteria, {
|
|
408
415
|
curatorSynthesis: isCuratorSynthesis,
|
|
409
416
|
blindEvaluation: isBlindEvaluation,
|
|
410
417
|
priorTurnBaseline,
|
|
411
418
|
compositeMergeDirective,
|
|
419
|
+
fileScoringDirective,
|
|
412
420
|
consensusReport: multiJudgeResult?.consensusReport,
|
|
413
421
|
})
|
|
414
422
|
const synthPromptTokens = Math.max(50, Math.ceil(synthesisPrompt.length / 4))
|
|
@@ -550,29 +558,26 @@ export async function runMoAPipeline({
|
|
|
550
558
|
let benchmarkReport = null
|
|
551
559
|
if (preset?.report_generation_enabled) {
|
|
552
560
|
benchmarkReport = generateMoABenchmarkReport({
|
|
553
|
-
runId,
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
durationMs,
|
|
558
|
-
usage: usageSummary,
|
|
559
|
-
costUsd: usageSummary.totalCostUsd,
|
|
560
|
-
candidates: referenceOutputs,
|
|
561
|
-
winningIndex,
|
|
562
|
-
winningLabel: winnerModel,
|
|
563
|
-
consensus: multiJudgeResult,
|
|
564
|
-
testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
561
|
+
runId, timestamp: new Date().toISOString(), preset: preset?.name || 'default',
|
|
562
|
+
prompt: userPrompt, durationMs, usage: usageSummary, costUsd: usageSummary.totalCostUsd,
|
|
563
|
+
candidates: referenceOutputs, winningIndex, winningLabel: winnerModel,
|
|
564
|
+
consensus: multiJudgeResult, testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
565
565
|
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
566
566
|
})
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
// Feature 10: Interactive Workbench candidate file quality scores
|
|
570
|
+
const fileScores = (preset?.interactive_workbench_enabled)
|
|
571
|
+
? parseFileScores(synthesizedText, referenceOutputs)
|
|
572
|
+
: null
|
|
573
|
+
|
|
569
574
|
try {
|
|
570
575
|
await recordMoaRunAsync({
|
|
571
576
|
id: runId, cwd, prompt: userPrompt, preset: preset?.name || 'default', isRefinement,
|
|
572
577
|
candidates: candidatesForHistory(referenceOutputs),
|
|
573
578
|
aggregator: { provider: chosenJudge.provider, model: chosenJudge.model, usage: aggUsage, costUsd: aggUsage.costUsd },
|
|
574
579
|
winnerIndex: winningIndex, winnerModel, promotedFiles, benchmarkReport, consensus: multiJudgeResult,
|
|
575
|
-
totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
|
|
580
|
+
fileScores, totalTokens: usageSummary.totalTokens, totalCostUsd: usageSummary.totalCostUsd, durationMs,
|
|
576
581
|
}, historyFilePath || undefined)
|
|
577
582
|
} catch (histErr) {
|
|
578
583
|
logger.warn('[dsh-moa] Failed to record run in history:', histErr)
|
|
@@ -582,7 +587,7 @@ export async function runMoAPipeline({
|
|
|
582
587
|
kind: 'synthesis', content: synthesizedText, aggregator: isFastMode ? 'Fast Mode (Direct)' : finalAggLabel,
|
|
583
588
|
references: referenceOutputs, presetName: preset?.name || 'default', isRefinement, isFastMode, isCuratorSynthesis,
|
|
584
589
|
recommendedAssembler, winningIndex, winnerModel, promotedFiles, runId, allowCandidateOverride,
|
|
585
|
-
benchmarkReport, consensus: multiJudgeResult, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
590
|
+
benchmarkReport, consensus: multiJudgeResult, fileScores, ...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
586
591
|
usage: usageSummary, skippedFiles: collectedCtx?.skippedFiles || 0, skippedList: collectedCtx?.skippedList || [], durationMs,
|
|
587
592
|
}
|
|
588
593
|
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Test Gate OS & Node Sandbox (#260)
|
|
3
|
+
*
|
|
4
|
+
* Implements OS-level isolation probing, permission model flags,
|
|
5
|
+
* fence module generation, environment sanitization, and evasion inspection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import cp from 'node:child_process'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import fsSync from 'node:fs'
|
|
11
|
+
import { logger } from './logger.js'
|
|
12
|
+
|
|
13
|
+
export 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
|
|
14
|
+
|
|
15
|
+
export const NODE_OPTIONS_EVASION = /(?:\benv\b[^|;\n]*(?:-[^\s]*u\s*NODE_OPTIONS|--unset=NODE_OPTIONS)|\bunset\s+NODE_OPTIONS|\bexport\s+NODE_OPTIONS\s*=|\bNODE_OPTIONS\s*=)/i
|
|
16
|
+
|
|
17
|
+
export const DANGEROUS_BINARIES = /\b(?:sudo|su|bwrap|chroot|systemd-run)\b/i
|
|
18
|
+
|
|
19
|
+
export 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)(?:\/|$)/
|
|
20
|
+
|
|
21
|
+
export const SAFE_SYS_DIRS = /^\/(?:usr\/(?:bin|lib|include)|bin|lib|lib64|etc|dev)(?:\/|$)/
|
|
22
|
+
|
|
23
|
+
export const SENSITIVE_EXTS = /\.(?:txt|json|env|npmrc|key|ssh|md)/i
|
|
24
|
+
|
|
25
|
+
export const PATH_TOKEN_REGEX = /(?:[a-zA-Z]:[\\/][^\s"'<>]+|(?<![\w.@/\\])\/(?:[a-zA-Z0-9_\-.]+\/)*[a-zA-Z0-9_\-.]+|(?<![\w.@/\\])\.\.[\\/][^\s"'<>]*)/g
|
|
26
|
+
|
|
27
|
+
export const OCTAL_HEX_ESCAPE = /\\(?:([0-7]{1,3})|x([0-9a-fA-F]{2}))/g
|
|
28
|
+
|
|
29
|
+
let cachedOsSandbox = null
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks whether OS-level unprivileged sandbox (bubblewrap) is fully operational (#260).
|
|
33
|
+
*/
|
|
34
|
+
export function isOsSandboxAvailable() {
|
|
35
|
+
if (cachedOsSandbox !== null) return cachedOsSandbox
|
|
36
|
+
if (process.platform !== 'linux') {
|
|
37
|
+
cachedOsSandbox = false
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const probe = cp.spawnSync(
|
|
42
|
+
'bwrap',
|
|
43
|
+
['--ro-bind', '/usr', '/usr', '--proc', '/proc', '--dev', '/dev', '--', 'true'],
|
|
44
|
+
{ timeout: 2000, stdio: 'ignore' }
|
|
45
|
+
)
|
|
46
|
+
cachedOsSandbox = probe.status === 0
|
|
47
|
+
} catch (probeErr) {
|
|
48
|
+
logger.debug('[dsh-moa:sandbox] OS sandbox probe error:', probeErr?.message || probeErr)
|
|
49
|
+
cachedOsSandbox = false
|
|
50
|
+
}
|
|
51
|
+
return cachedOsSandbox
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves package manager runner commands (e.g. `npm test`) to direct script invocations
|
|
56
|
+
* to allow execution under Node permission model without requiring --allow-child-process (#260).
|
|
57
|
+
*/
|
|
58
|
+
export function resolveTestCommand({ testCommand, executionDir }) {
|
|
59
|
+
if (!testCommand || typeof testCommand !== 'string') return ''
|
|
60
|
+
const trimmed = testCommand.trim()
|
|
61
|
+
const npmMatch = trimmed.match(/^(?:npm\s+(?:run\s+)?test|npm\s+t|pnpm\s+(?:run\s+)?test|pnpm\s+t|yarn\s+test)(?:\s+--?\s*(.*))?$/i)
|
|
62
|
+
if (npmMatch && executionDir) {
|
|
63
|
+
const candPkgFile = path.join(executionDir, 'package.json')
|
|
64
|
+
if (fsSync.existsSync(candPkgFile)) {
|
|
65
|
+
try {
|
|
66
|
+
const pkgObj = JSON.parse(fsSync.readFileSync(candPkgFile, 'utf8'))
|
|
67
|
+
const scriptCmd = pkgObj.scripts?.test
|
|
68
|
+
if (scriptCmd && typeof scriptCmd === 'string') {
|
|
69
|
+
const extraArgs = npmMatch[1] ? ` ${npmMatch[1]}` : ''
|
|
70
|
+
return scriptCmd.trim() + extraArgs
|
|
71
|
+
}
|
|
72
|
+
} catch (pkgErr) {
|
|
73
|
+
logger.debug('[dsh-moa:sandbox] package.json parse error in resolveTestCommand:', pkgErr?.message || pkgErr)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return trimmed
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Inspects a command or script string for obfuscation or security environment evasion (#161, #260).
|
|
82
|
+
*/
|
|
83
|
+
export function isSandboxEvasion(text) {
|
|
84
|
+
if (!text || typeof text !== 'string') return null
|
|
85
|
+
if (COMPUTED_SHELL_EVASION.test(text)) {
|
|
86
|
+
return 'obfuscated or computed command execution'
|
|
87
|
+
}
|
|
88
|
+
if (NODE_OPTIONS_EVASION.test(text)) {
|
|
89
|
+
return 'attempts to bypass sandbox or strip security environment (NODE_OPTIONS)'
|
|
90
|
+
}
|
|
91
|
+
if (DANGEROUS_BINARIES.test(text)) {
|
|
92
|
+
return 'unauthorized privileged or sandbox-escape binary execution'
|
|
93
|
+
}
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decodes octal and hex escape sequences in strings.
|
|
99
|
+
*/
|
|
100
|
+
export function decodeEscapedStrings(str) {
|
|
101
|
+
if (!str || typeof str !== 'string') return ''
|
|
102
|
+
const normalized = str.split(/\\+/).join('\\')
|
|
103
|
+
return normalized.replace(OCTAL_HEX_ESCAPE, (m, oct, hex) => {
|
|
104
|
+
if (oct) return String.fromCharCode(parseInt(oct, 8))
|
|
105
|
+
if (hex) return String.fromCharCode(parseInt(hex, 16))
|
|
106
|
+
return m
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates that an executed script or command does not reference files outside allowed roots (#161, #225, #260).
|
|
112
|
+
*/
|
|
113
|
+
export function scanPathTokensForLeak(text, allowedRoots, executionDir) {
|
|
114
|
+
if (!text || typeof text !== 'string') return null
|
|
115
|
+
const checkText = (t) => {
|
|
116
|
+
const sanitized = t.replace(/https?:\/\/[^\s"'<>]+/gi, '').replace(/\b(?:node|file|npm|data):[^\s"'<>]+/gi, '')
|
|
117
|
+
const matches = sanitized.match(PATH_TOKEN_REGEX) || []
|
|
118
|
+
for (const raw of matches) {
|
|
119
|
+
if (SAFE_SYS_DIRS.test(raw) && !SENSITIVE_EXTS.test(raw)) continue
|
|
120
|
+
const isAbs = path.isAbsolute(raw)
|
|
121
|
+
if ((!isAbs && !raw.startsWith('..')) || (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(raw))) continue
|
|
122
|
+
const resolved = path.resolve(executionDir, raw)
|
|
123
|
+
if (!allowedRoots.some((r) => r && (resolved === r || resolved.startsWith(r + path.sep)))) return raw
|
|
124
|
+
}
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
const rawLeak = checkText(text)
|
|
128
|
+
if (rawLeak) return rawLeak
|
|
129
|
+
const segmented = text.match(/\bprintf\s+(?:%s|['"]%s['"])\s+((?:['"][^'"]*['"]\s*)+)/i)
|
|
130
|
+
if (segmented) {
|
|
131
|
+
const assembled = (segmented[1].match(/['"]([^'"]*)['"]/g) || []).map((s) => s.slice(1, -1)).join('')
|
|
132
|
+
const assembledLeak = checkText(assembled)
|
|
133
|
+
if (assembledLeak) return assembledLeak
|
|
134
|
+
}
|
|
135
|
+
const decoded = decodeEscapedStrings(text)
|
|
136
|
+
return decoded !== text ? checkText(decoded) : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sanitizes process environment for untrusted candidate tests (#244, #260).
|
|
141
|
+
*/
|
|
142
|
+
export function getScrubbedEnv(overrides = {}) {
|
|
143
|
+
const safeVars = new Set([
|
|
144
|
+
'PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TMPDIR', 'TEMP', 'TMP',
|
|
145
|
+
'USER', 'LOGNAME', 'HOME', 'SHELL', 'TERM', 'NODE_ENV', 'SYSTEMROOT',
|
|
146
|
+
'WINDIR', 'COMSPEC', 'PATHEXT',
|
|
147
|
+
])
|
|
148
|
+
const scrubbed = {}
|
|
149
|
+
for (const [key, val] of Object.entries(process.env)) {
|
|
150
|
+
if (!val || key.startsWith('MOA_') || key.startsWith('DSH_') || key.startsWith('SSH_')) continue
|
|
151
|
+
if (/(API_KEY|TOKEN|SECRET|PASSWORD|PASSWD|AUTH|CREDENTIAL|PRIVATE|KEY|ACCESS)/i.test(key)) {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (safeVars.has(key) || key.startsWith('LC_')) {
|
|
155
|
+
scrubbed[key] = val
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { ...scrubbed, ...overrides }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds Node.js permission model CLI options (#260).
|
|
163
|
+
*
|
|
164
|
+
* CRITICAL SECURITY CONTRACT:
|
|
165
|
+
* When OS-level container isolation is NOT active, --allow-child-process MUST NOT be used,
|
|
166
|
+
* because child processes can strip NODE_OPTIONS and bypass the permission model.
|
|
167
|
+
*/
|
|
168
|
+
export function buildPermOpts({ allowedReads, executionDir, effectiveHome, fenceFile, hasOsSandbox = false }) {
|
|
169
|
+
const readFlags = allowedReads.map((p) => `--allow-fs-read=${p}`).join(' ')
|
|
170
|
+
const writeFlags = `--allow-fs-write=${executionDir} --allow-fs-write=${effectiveHome}`
|
|
171
|
+
const childFlag = hasOsSandbox ? ' --allow-child-process' : ''
|
|
172
|
+
return `--permission ${readFlags} ${writeFlags}${childFlag} --require ${fenceFile}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Generates the source code for the ephemeral runtime fence module (#161, #260).
|
|
177
|
+
*/
|
|
178
|
+
export function generateFenceCode({ allowedReads, hasOsSandbox = false }) {
|
|
179
|
+
const allowedJson = JSON.stringify(allowedReads)
|
|
180
|
+
const evasionPattern = JSON.stringify(COMPUTED_SHELL_EVASION.source)
|
|
181
|
+
const nodeOptsPattern = JSON.stringify(NODE_OPTIONS_EVASION.source)
|
|
182
|
+
const unixRootPattern = JSON.stringify(UNIX_ROOT_PREFIX.source)
|
|
183
|
+
const sysDirsPattern = JSON.stringify(SAFE_SYS_DIRS.source)
|
|
184
|
+
const sensExtsPattern = JSON.stringify(SENSITIVE_EXTS.source)
|
|
185
|
+
const pathTokenPattern = JSON.stringify(PATH_TOKEN_REGEX.source)
|
|
186
|
+
const octalHexPattern = JSON.stringify(OCTAL_HEX_ESCAPE.source)
|
|
187
|
+
|
|
188
|
+
return [
|
|
189
|
+
"const cp = require('node:child_process');",
|
|
190
|
+
"const path = require('node:path');",
|
|
191
|
+
`const allowed = ${allowedJson};`,
|
|
192
|
+
`const hasOsSandbox = ${hasOsSandbox ? 'true' : 'false'};`,
|
|
193
|
+
"",
|
|
194
|
+
`const COMPUTED_SHELL_EVASION = new RegExp(${evasionPattern}, 'i');`,
|
|
195
|
+
`const NODE_OPTIONS_EVASION = new RegExp(${nodeOptsPattern}, 'i');`,
|
|
196
|
+
`const UNIX_ROOT_PREFIX = new RegExp(${unixRootPattern});`,
|
|
197
|
+
`const SAFE_SYS_DIRS = new RegExp(${sysDirsPattern});`,
|
|
198
|
+
`const SENSITIVE_EXTS = new RegExp(${sensExtsPattern}, 'i');`,
|
|
199
|
+
`const PATH_TOKEN_REGEX = new RegExp(${pathTokenPattern}, 'g');`,
|
|
200
|
+
`const OCTAL_HEX_ESCAPE = new RegExp(${octalHexPattern}, 'g');`,
|
|
201
|
+
"",
|
|
202
|
+
"function decodeEscaped(str) {",
|
|
203
|
+
" if (!str || typeof str !== 'string') return '';",
|
|
204
|
+
" const norm = str.replace(/\\+/g, '\\\\');",
|
|
205
|
+
" return norm.replace(OCTAL_HEX_ESCAPE, (m, oct, hex) => {",
|
|
206
|
+
" if (oct) return String.fromCharCode(parseInt(oct, 8));",
|
|
207
|
+
" if (hex) return String.fromCharCode(parseInt(hex, 16));",
|
|
208
|
+
" return m;",
|
|
209
|
+
" });",
|
|
210
|
+
"}",
|
|
211
|
+
"",
|
|
212
|
+
"function checkSinglePath(p) {",
|
|
213
|
+
" if (!p || typeof p !== 'string') return true;",
|
|
214
|
+
" if (SAFE_SYS_DIRS.test(p) && !SENSITIVE_EXTS.test(p)) return true;",
|
|
215
|
+
" const isAbs = path.isAbsolute(p);",
|
|
216
|
+
" const isUp = p.startsWith('..');",
|
|
217
|
+
" if (!isAbs && !isUp) return true;",
|
|
218
|
+
" if (isAbs && process.platform !== 'win32' && !UNIX_ROOT_PREFIX.test(p)) return true;",
|
|
219
|
+
" const res = path.resolve(process.cwd(), p);",
|
|
220
|
+
" return allowed.some(root => root && (res === root || res.startsWith(root + path.sep)));",
|
|
221
|
+
"}",
|
|
222
|
+
"",
|
|
223
|
+
"function scanStringForPaths(str) {",
|
|
224
|
+
" if (!str || typeof str !== 'string') return true;",
|
|
225
|
+
" if (!checkSinglePath(str)) return false;",
|
|
226
|
+
" const dec = decodeEscaped(str);",
|
|
227
|
+
" if (dec !== str && !checkSinglePath(dec)) return false;",
|
|
228
|
+
" const matches = str.match(PATH_TOKEN_REGEX) || [];",
|
|
229
|
+
" for (const m of matches) {",
|
|
230
|
+
" if (!checkSinglePath(m)) return false;",
|
|
231
|
+
" }",
|
|
232
|
+
" if (dec !== str) {",
|
|
233
|
+
" const decMatches = dec.match(PATH_TOKEN_REGEX) || [];",
|
|
234
|
+
" for (const m of decMatches) {",
|
|
235
|
+
" if (!checkSinglePath(m)) return false;",
|
|
236
|
+
" }",
|
|
237
|
+
" }",
|
|
238
|
+
" return true;",
|
|
239
|
+
"}",
|
|
240
|
+
"",
|
|
241
|
+
"function guardChildCall(methodName, cmd, args) {",
|
|
242
|
+
" if (!hasOsSandbox) {",
|
|
243
|
+
" throw new Error('ERR_ACCESS_DENIED: child process execution restricted without OS-level sandbox: ' + methodName);",
|
|
244
|
+
" }",
|
|
245
|
+
" const full = [cmd, ...(Array.isArray(args) ? args : [])].join(' ');",
|
|
246
|
+
" if (COMPUTED_SHELL_EVASION.test(full)) {",
|
|
247
|
+
" throw new Error('ERR_ACCESS_DENIED: child process computed execution pattern blocked: ' + full);",
|
|
248
|
+
" }",
|
|
249
|
+
" if (NODE_OPTIONS_EVASION.test(full)) {",
|
|
250
|
+
" throw new Error('ERR_ACCESS_DENIED: child process attempts to strip security environment: ' + full);",
|
|
251
|
+
" }",
|
|
252
|
+
" if (cmd && !scanStringForPaths(String(cmd))) {",
|
|
253
|
+
" throw new Error('ERR_ACCESS_DENIED: child process command outside sandbox: ' + cmd);",
|
|
254
|
+
" }",
|
|
255
|
+
" if (Array.isArray(args)) {",
|
|
256
|
+
" for (const a of args) {",
|
|
257
|
+
" if (a && !scanStringForPaths(String(a))) {",
|
|
258
|
+
" throw new Error('ERR_ACCESS_DENIED: child process argument outside sandbox: ' + a);",
|
|
259
|
+
" }",
|
|
260
|
+
" }",
|
|
261
|
+
" }",
|
|
262
|
+
"}",
|
|
263
|
+
"",
|
|
264
|
+
"const origSpawn = cp.spawn; cp.spawn = function(cmd, args, opts) { guardChildCall('spawn', cmd, args); return origSpawn.apply(this, arguments); };",
|
|
265
|
+
"const origSpawnSync = cp.spawnSync; cp.spawnSync = function(cmd, args, opts) { guardChildCall('spawnSync', cmd, args); return origSpawnSync.apply(this, arguments); };",
|
|
266
|
+
"const origExecFile = cp.execFile; cp.execFile = function(file, args, opts, cb) { guardChildCall('execFile', file, args); return origExecFile.apply(this, arguments); };",
|
|
267
|
+
"const origExecFileSync = cp.execFileSync; cp.execFileSync = function(file, args, opts) { guardChildCall('execFileSync', file, args); return origExecFileSync.apply(this, arguments); };",
|
|
268
|
+
"const origExec = cp.exec; cp.exec = function(cmd, opts, cb) { guardChildCall('exec', cmd, [cmd]); return origExec.apply(this, arguments); };",
|
|
269
|
+
"const origExecSync = cp.execSync; cp.execSync = function(cmd, opts) { guardChildCall('execSync', cmd, [cmd]); return origExecSync.apply(this, arguments); };",
|
|
270
|
+
"const origFork = cp.fork; cp.fork = function(modulePath, args, opts) { guardChildCall('fork', modulePath, args); return origFork.apply(this, arguments); };",
|
|
271
|
+
"",
|
|
272
|
+
"const fsMod = require('node:fs');",
|
|
273
|
+
"function guardFs(p, isWrite) {",
|
|
274
|
+
" if (!p || typeof p !== 'string') return;",
|
|
275
|
+
" const isAbs = path.isAbsolute(p);",
|
|
276
|
+
" const isUp = p.startsWith('..');",
|
|
277
|
+
" if (!isAbs && !isUp) return;",
|
|
278
|
+
" const resolved = path.resolve(process.cwd(), p);",
|
|
279
|
+
" const checkRoots = isWrite ? [process.cwd(), process.env.HOME].filter(Boolean) : allowed;",
|
|
280
|
+
" const ok = checkRoots.some(r => r && (resolved === r || resolved.startsWith(r + path.sep)));",
|
|
281
|
+
" if (!ok) throw new Error('ERR_ACCESS_DENIED: fs access outside sandbox: ' + p);",
|
|
282
|
+
"}",
|
|
283
|
+
"const origRF = fsMod.readFileSync; fsMod.readFileSync = function(p) { guardFs(p, false); return origRF.apply(this, arguments); };",
|
|
284
|
+
"const origWF = fsMod.writeFileSync; fsMod.writeFileSync = function(p) { guardFs(p, true); return origWF.apply(this, arguments); };",
|
|
285
|
+
"const origAF = fsMod.appendFileSync; fsMod.appendFileSync = function(p) { guardFs(p, true); return origAF.apply(this, arguments); };",
|
|
286
|
+
].join('\n')
|
|
287
|
+
}
|
package/lib/moa-schema.js
CHANGED
|
@@ -62,6 +62,7 @@ export const PresetSchema = z.object({
|
|
|
62
62
|
report_generation_enabled: z.boolean().default(false),
|
|
63
63
|
local_fallback_enabled: z.boolean().default(false),
|
|
64
64
|
local_fallback_models: z.array(ModelSlotSchema).default([]),
|
|
65
|
+
interactive_workbench_enabled: z.boolean().default(false),
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
export const Config = z.object({
|