@goodandready/dsh-moa 0.2.51 → 0.2.53

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.
@@ -403,9 +403,9 @@ export async function cleanMoaWorkspaces(baseDir, options = {}) {
403
403
  if (!baseDir) return
404
404
  const rawRunId = typeof options === 'string' ? options : options?.runId
405
405
  const safeRunId = rawRunId ? String(rawRunId).trim().replace(/[^a-zA-Z0-9_-]/g, '') : null
406
- const moaRoot = path.join(baseDir, '.moa')
406
+ const moaRoot = assertPathContained(baseDir, '.moa')
407
407
  if (safeRunId) {
408
- await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(path.join(moaRoot, safeRunId), { recursive: true, force: true }))
408
+ await bestEffort('cleanMoaWorkspaces runId', () => fs.rm(assertPathContained(moaRoot, safeRunId), { recursive: true, force: true }))
409
409
  await bestEffort('clean empty moaRoot', async () => {
410
410
  if ((await fs.readdir(moaRoot)).length === 0) await fs.rmdir(moaRoot)
411
411
  })
package/lib/moa-budget.js CHANGED
@@ -87,15 +87,38 @@ 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, 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) +
94
- (typeof judgeCriteria === 'string' ? judgeCriteria.length : 0) +
95
- baseOverhead
90
+ export function calcSynthesisPromptTokens(firstArg = '', maybeRefs = [], maybeCriteria = '', maybeOverhead = 2600, maybeOptions = {}) {
91
+ let userPrompt, referenceOutputs, judgeCriteria, projectContext, priorTurnBaseline, consensusReport, compositeMergeDirective, baseOverhead
92
+
93
+ if (typeof firstArg === 'object' && firstArg !== null) {
94
+ userPrompt = firstArg.userPrompt || ''
95
+ referenceOutputs = firstArg.referenceOutputs || []
96
+ judgeCriteria = firstArg.judgeCriteria || ''
97
+ projectContext = firstArg.projectContext || ''
98
+ priorTurnBaseline = firstArg.priorTurnBaseline
99
+ consensusReport = firstArg.consensusReport
100
+ compositeMergeDirective = firstArg.compositeMergeDirective
101
+ baseOverhead = typeof firstArg.overhead === 'number' ? firstArg.overhead : 2600
102
+ } else {
103
+ userPrompt = typeof firstArg === 'string' ? firstArg : ''
104
+ referenceOutputs = Array.isArray(maybeRefs) ? maybeRefs : []
105
+ judgeCriteria = typeof maybeCriteria === 'string' ? maybeCriteria : ''
106
+ const opts = (typeof maybeOverhead === 'object' && maybeOverhead !== null) ? maybeOverhead : (maybeOptions || {})
107
+ projectContext = opts.projectContext || ''
108
+ priorTurnBaseline = opts.priorTurnBaseline
109
+ consensusReport = opts.consensusReport
110
+ compositeMergeDirective = opts.compositeMergeDirective
111
+ baseOverhead = typeof maybeOverhead === 'number' ? maybeOverhead : 2600
112
+ }
113
+
114
+ let promptChars = userPrompt.length + judgeCriteria.length + (projectContext?.length || 0) + baseOverhead
96
115
 
97
116
  if (Array.isArray(referenceOutputs)) {
117
+ if (referenceOutputs.some((r) => r && r.testResult)) {
118
+ promptChars += 550 // TEST_GATE_DIRECTIVE overhead
119
+ }
98
120
  for (const r of referenceOutputs) {
121
+ if (!r) continue
99
122
  promptChars += (r.text?.length || 0)
100
123
  if (r.testResult) {
101
124
  promptChars += 150 + (r.testResult.output?.length || 0)
@@ -105,12 +128,12 @@ export function calcSynthesisPromptTokens(userPrompt = '', referenceOutputs = []
105
128
  }
106
129
  }
107
130
  }
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)
131
+ if (priorTurnBaseline) promptChars += priorTurnBaseline.length + 50
132
+ if (consensusReport) {
133
+ const crText = typeof consensusReport === 'string' ? consensusReport : JSON.stringify(consensusReport)
111
134
  promptChars += crText.length + 50
112
135
  }
113
- if (opts.compositeMergeDirective) promptChars += opts.compositeMergeDirective.length + 50
136
+ if (compositeMergeDirective) promptChars += compositeMergeDirective.length + 50
114
137
 
115
138
  return Math.max(50, Math.ceil(promptChars / 4))
116
139
  }
@@ -301,8 +324,23 @@ export function createBudgetAbortPayload({
301
324
  multiJudgeResult = null,
302
325
  }) {
303
326
  const aggLabel = (primaryJudge?.provider && primaryJudge?.model) ? `${primaryJudge.provider}:${primaryJudge.model}` : "none";
304
- const refTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0);
305
- const panelTokens = multiJudgeResult?.totalUsage?.totalTokens || 0;
327
+ const refTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens ?? ((r.usage?.inputTokens || 0) + (r.usage?.outputTokens || 0))), 0);
328
+ const refInTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.inputTokens || r.usage?.promptTokens || 0), 0);
329
+ const refOutTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.outputTokens || r.usage?.completionTokens || 0), 0);
330
+
331
+ const mjUsage = multiJudgeResult?.totalUsage;
332
+ const panelTokens = mjUsage?.totalTokens ?? ((mjUsage?.inputTokens || 0) + (mjUsage?.outputTokens || 0));
333
+ const panelInTokens = mjUsage?.inputTokens || mjUsage?.promptTokens || 0;
334
+ const panelOutTokens = mjUsage?.outputTokens || mjUsage?.completionTokens || 0;
335
+ const panelCostUsd = mjUsage?.costUsd || multiJudgeResult?.totalCostUsd || 0;
336
+
337
+ const refCost = referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0);
338
+ const totalCostUsd = Number(Math.max(currentSpentUsd, refCost + panelCostUsd).toFixed(5));
339
+
340
+ const totalTokens = refTokens + panelTokens;
341
+ const totalInputTokens = refInTokens + panelInTokens;
342
+ const totalOutputTokens = refOutTokens + panelOutTokens;
343
+
306
344
  return {
307
345
  kind: "failure",
308
346
  content: `🛑 Budget Guardrail Abort: ${reason}`,
@@ -315,11 +353,13 @@ export function createBudgetAbortPayload({
315
353
  winnerModel: "none",
316
354
  promotedFiles: [],
317
355
  usage: {
318
- totalTokens: refTokens + panelTokens,
319
- totalCostUsd: Number(currentSpentUsd.toFixed(5)),
356
+ totalTokens,
357
+ totalInputTokens,
358
+ totalOutputTokens,
359
+ totalCostUsd,
320
360
  candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
321
361
  aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
322
- ...(multiJudgeResult ? { multiJudge: multiJudgeResult.totalUsage || { totalTokens: panelTokens } } : {}),
362
+ ...(multiJudgeResult ? { multiJudge: mjUsage || { inputTokens: panelInTokens, outputTokens: panelOutTokens, totalTokens: panelTokens, costUsd: panelCostUsd } } : {}),
323
363
  },
324
364
  multiJudgeResult: multiJudgeResult || undefined,
325
365
  skippedFiles: collectedCtx?.skippedFiles || 0,
@@ -420,7 +420,11 @@ export async function runMultiJudgePhase({
420
420
  const panelMaxTokens = Math.min(typeof preset.max_tokens === 'number' && preset.max_tokens > 0 ? preset.max_tokens : 2048, 2048)
421
421
 
422
422
  if (preset?.budget_guard_enabled && preset?.max_budget_usd > 0) {
423
- const promptTokens = calcSynthesisPromptTokens(userPrompt, successfulRefs, judgeCriteria)
423
+ const promptTokens = calcSynthesisPromptTokens({
424
+ userPrompt,
425
+ referenceOutputs: successfulRefs,
426
+ judgeCriteria,
427
+ })
424
428
  const panelEst = estimatePanelCost(panelJudges, successfulRefs.length, prices, panelMaxTokens, promptTokens)
425
429
  const downEst = estimateDownstreamCost(primaryJudge, [], successfulRefs.length, prices, maxTokens, promptTokens)
426
430
  const phaseCheck = checkPhaseBudget({
package/lib/routes.js CHANGED
@@ -3,8 +3,9 @@
3
3
  */
4
4
 
5
5
  import path from 'node:path'
6
- import { getMoaHistory, getMoaLeaderboard, getMoaRunById, exportMoaHistory } from './history.js'
7
- import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl } from './file-workspace.js'
6
+ import os from 'node:os'
7
+ import { getMoaHistory, getMoaLeaderboard, getMoaRunById, exportMoaHistory, isTestEnvironment } from './history.js'
8
+ import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl, assertPathContained } from './file-workspace.js'
8
9
  import { isSafeWriteRequest } from './updater.js'
9
10
  import { runMoAPipeline } from './moa-runner.js'
10
11
  import { generateMoABenchmarkReport } from './moa-report.js'
@@ -30,7 +31,21 @@ function readBody(req) {
30
31
  })
31
32
  }
32
33
 
33
- export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine }) {
34
+ function isAllowedWorkspace(targetCwd, allowedRoots) {
35
+ if (!targetCwd || typeof targetCwd !== 'string') return false
36
+ for (const root of allowedRoots) {
37
+ if (!root) continue
38
+ try {
39
+ assertPathContained(root, targetCwd)
40
+ return true
41
+ } catch {
42
+ // not within root
43
+ }
44
+ }
45
+ return false
46
+ }
47
+
48
+ export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine, workspaceRoot }) {
34
49
 
35
50
  ctx.effect(() => {
36
51
  return ctx.webServer.register({
@@ -535,13 +550,28 @@ ctx.effect(() => {
535
550
  const presetName = payload.preset || cfg.default_preset || 'default'
536
551
  const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
537
552
 
553
+ const baseWorkspaceRoot = workspaceRoot || ctx.baseDir || ctx.workspaceDir || process.cwd()
554
+ const allowedRoots = [
555
+ baseWorkspaceRoot,
556
+ process.cwd(),
557
+ cfg.workspace_root,
558
+ ...(Array.isArray(cfg.allowed_workspaces) ? cfg.allowed_workspaces : (cfg.allowed_workspaces ? [cfg.allowed_workspaces] : [])),
559
+ ...(isTestEnvironment() ? [os.tmpdir()] : []),
560
+ ].filter(Boolean)
561
+
562
+ const targetCwd = payload.cwd ? path.resolve(payload.cwd) : baseWorkspaceRoot
563
+ if (payload.cwd && !isAllowedWorkspace(targetCwd, allowedRoots)) {
564
+ writeJson(res, 403, { ok: false, error: 'Forbidden: cwd escapes allowed workspace boundary' })
565
+ return
566
+ }
567
+
538
568
  const result = await runMoAPipeline({
539
569
  userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
540
- cwd: payload.cwd || process.cwd(), prices: cfg.prices || {}, liveCanvas,
570
+ cwd: targetCwd, prices: cfg.prices || {}, liveCanvas,
541
571
  webServerPort: webServerPort || ctx.webServer?.port,
542
572
  timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
543
- timeMachineUrl: payload.timeMachineUrl, force: payload.force,
544
- checkpoint: payload.checkpoint,
573
+ timeMachineUrl: payload.timeMachineUrl, force: false,
574
+ checkpoint: true,
545
575
  runId: payload.runId,
546
576
  })
547
577
 
package/lib/updater.js CHANGED
@@ -44,8 +44,7 @@ export function isTrustedUpdateRequest(request) {
44
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
- const isLoop = !remote || isLoopback(remote);
48
- if (!isLoop) return false;
47
+ if (!remote || !isLoopback(remote)) return false;
49
48
 
50
49
  const site = header(request, 'sec-fetch-site');
51
50
  if (site === 'cross-site') return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.51",
3
+ "version": "0.2.53",
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",