@goodandready/dsh-moa 0.2.59 → 0.2.61
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/access.js +26 -6
- package/lib/file-workspace.js +33 -593
- package/lib/index.js +1 -0
- package/lib/routes/history.js +81 -0
- package/lib/routes/pipeline.js +91 -0
- package/lib/routes/presets.js +241 -0
- package/lib/routes/workbench.js +197 -0
- package/lib/routes.js +17 -582
- package/lib/workspace/checkpoint.js +51 -0
- package/lib/workspace/diff.js +94 -0
- package/lib/workspace/fs.js +284 -0
- package/lib/workspace/scanner.js +100 -0
- package/lib/workspace/security.js +80 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -324,6 +324,7 @@ export function apply(ctx, config) {
|
|
|
324
324
|
}, 'dsh-moa: llm stream interceptor')
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
export { runMoAPipeline } from './moa-runner.js'
|
|
327
328
|
export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
|
|
328
329
|
export {
|
|
329
330
|
COMPANION_PLUGINS,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getMoaHistory, getMoaLeaderboard, exportMoaHistory } from '../history.js'
|
|
2
|
+
import { writeJson, assertAuthorizedRequest } from '../access.js'
|
|
3
|
+
|
|
4
|
+
export function registerHistoryRoutes(ctx, { live }) {
|
|
5
|
+
ctx.effect(() => {
|
|
6
|
+
return ctx.webServer.register({
|
|
7
|
+
kind: 'exact',
|
|
8
|
+
path: '/dsh-moa/history',
|
|
9
|
+
handler: async (req, res) => {
|
|
10
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
11
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(req.url, 'http://localhost')
|
|
14
|
+
const limit = Math.max(1, Math.min(100, parseInt(u.searchParams.get('limit') || '20', 10) || 20))
|
|
15
|
+
const offset = Math.max(0, parseInt(u.searchParams.get('offset') || '0', 10) || 0)
|
|
16
|
+
const data = getMoaHistory(limit, offset)
|
|
17
|
+
writeJson(res, 200, { ok: true, ...data })
|
|
18
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
}, 'dsh-moa: history route')
|
|
22
|
+
|
|
23
|
+
// Route: /dsh-moa/leaderboard
|
|
24
|
+
|
|
25
|
+
ctx.effect(() => {
|
|
26
|
+
return ctx.webServer.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dsh-moa/leaderboard',
|
|
29
|
+
handler: async (req, res) => {
|
|
30
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
31
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
34
|
+
const preset = url.searchParams.get('preset') || null
|
|
35
|
+
const format = url.searchParams.get('format') || 'json'
|
|
36
|
+
|
|
37
|
+
if (format === 'csv') {
|
|
38
|
+
const csv = exportMoaHistory(undefined, { format: 'csv', preset })
|
|
39
|
+
res.writeHead(200, {
|
|
40
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
41
|
+
'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
|
|
42
|
+
})
|
|
43
|
+
res.end(csv)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = getMoaLeaderboard(undefined, { preset })
|
|
48
|
+
writeJson(res, 200, { ok: true, ...data })
|
|
49
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}, 'dsh-moa: leaderboard route')
|
|
53
|
+
|
|
54
|
+
// Route: /dsh-moa/promote (Candidate Override Action)
|
|
55
|
+
|
|
56
|
+
ctx.effect(() => {
|
|
57
|
+
return ctx.webServer.register({
|
|
58
|
+
kind: 'exact',
|
|
59
|
+
path: '/dsh-moa/history/export',
|
|
60
|
+
handler: async (req, res) => {
|
|
61
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
62
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
65
|
+
const preset = url.searchParams.get('preset') || null
|
|
66
|
+
const format = url.searchParams.get('format') || 'json'
|
|
67
|
+
const data = exportMoaHistory(undefined, { format, preset })
|
|
68
|
+
|
|
69
|
+
const isCsv = format === 'csv'
|
|
70
|
+
res.writeHead(200, {
|
|
71
|
+
'Content-Type': isCsv ? 'text/csv; charset=utf-8' : 'application/json; charset=utf-8',
|
|
72
|
+
'Content-Disposition': `attachment; filename="moa-history.${isCsv ? 'csv' : 'json'}"`,
|
|
73
|
+
})
|
|
74
|
+
res.end(data)
|
|
75
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}, 'dsh-moa: history export route')
|
|
79
|
+
|
|
80
|
+
// Route: /dsh-moa/runs/<id> (run replay by id and interactive workbench)
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import { isTestEnvironment } from '../history.js'
|
|
4
|
+
import { isLoopbackUrl } from '../file-workspace.js'
|
|
5
|
+
import { runMoAPipeline } from '../moa-runner.js'
|
|
6
|
+
import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from '../access.js'
|
|
7
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function registerPipelineRoutes(ctx, { live, callLlm, liveCanvas, webServerPort, timeMachineEngine, workspaceRoot }) {
|
|
11
|
+
let _lastRunTimestamp = 0
|
|
12
|
+
const RUN_MIN_INTERVAL_MS = 1000
|
|
13
|
+
|
|
14
|
+
ctx.effect(() => {
|
|
15
|
+
return ctx.webServer.register({
|
|
16
|
+
kind: 'exact',
|
|
17
|
+
path: '/dsh-moa/run',
|
|
18
|
+
handler: async (req, res) => {
|
|
19
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
20
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
21
|
+
if (!isSafeWriteRequest(req)) {
|
|
22
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
if (now - _lastRunTimestamp < RUN_MIN_INTERVAL_MS) {
|
|
27
|
+
writeJson(res, 429, { ok: false, error: 'Rate limit exceeded: please wait before starting another run' })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readBody(req)
|
|
33
|
+
const payload = JSON.parse(raw)
|
|
34
|
+
const prompt = payload.prompt || ''
|
|
35
|
+
if (!prompt.trim()) {
|
|
36
|
+
writeJson(res, 400, { ok: false, error: 'Empty prompt' })
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
_lastRunTimestamp = now
|
|
40
|
+
|
|
41
|
+
const cfg = live()
|
|
42
|
+
if (cfg.enabled === false) {
|
|
43
|
+
writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (payload.timeMachineUrl && !isLoopbackUrl(payload.timeMachineUrl)) {
|
|
47
|
+
writeJson(res, 400, { ok: false, error: 'Invalid timeMachineUrl: loopback host required' })
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
51
|
+
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
52
|
+
|
|
53
|
+
const baseWorkspaceRoot = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
54
|
+
const allowedRoots = [
|
|
55
|
+
baseWorkspaceRoot,
|
|
56
|
+
process.cwd(),
|
|
57
|
+
cfg.workspace_root,
|
|
58
|
+
...(Array.isArray(cfg.allowed_workspaces) ? cfg.allowed_workspaces : (cfg.allowed_workspaces ? [cfg.allowed_workspaces] : [])),
|
|
59
|
+
...(isTestEnvironment() ? [os.tmpdir()] : []),
|
|
60
|
+
].filter(Boolean)
|
|
61
|
+
|
|
62
|
+
const targetCwd = payload.cwd ? path.resolve(payload.cwd) : baseWorkspaceRoot
|
|
63
|
+
if (payload.cwd && !isAllowedWorkspace(targetCwd, allowedRoots)) {
|
|
64
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cwd escapes allowed workspace boundary' })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await runMoAPipeline({
|
|
69
|
+
userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
|
|
70
|
+
cwd: targetCwd, prices: cfg.prices || {}, liveCanvas,
|
|
71
|
+
webServerPort: webServerPort || ctx.webServer?.port,
|
|
72
|
+
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
73
|
+
timeMachineUrl: payload.timeMachineUrl, force: false,
|
|
74
|
+
checkpoint: true,
|
|
75
|
+
runId: payload.runId,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (result?.skippedFiles > 0) {
|
|
79
|
+
ctx.logger?.warn?.(`[dsh-moa] Workspace scan skipped ${result.skippedFiles} file(s): ${result.skippedList.join(', ')}`)
|
|
80
|
+
}
|
|
81
|
+
if (result?.error) {
|
|
82
|
+
writeJson(res, 500, { ok: false, error: result.error?.message || String(result.error) })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
87
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}, 'dsh-moa: run pipeline route')
|
|
91
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
|
|
2
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
3
|
+
import { resolvePresetForPrompt } from '../moa-router.js'
|
|
4
|
+
|
|
5
|
+
let _modelsCache = { timestamp: 0, models: [] }
|
|
6
|
+
const MODELS_CACHE_TTL = 60 * 1000
|
|
7
|
+
|
|
8
|
+
export function registerPresetRoutes(ctx, { live, callLlm, saveConfig, Config, plainConfig }) {
|
|
9
|
+
ctx.effect(() => {
|
|
10
|
+
return ctx.webServer.register({
|
|
11
|
+
kind: 'exact',
|
|
12
|
+
path: '/dsh-moa/status',
|
|
13
|
+
handler: (req, res) => {
|
|
14
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
15
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
16
|
+
try {
|
|
17
|
+
const cfg = live()
|
|
18
|
+
writeJson(res, 200, {
|
|
19
|
+
ok: true,
|
|
20
|
+
enabled: cfg.enabled !== false,
|
|
21
|
+
defaultPreset: cfg.default_preset || 'default',
|
|
22
|
+
presetsCount: (cfg.presets || []).length,
|
|
23
|
+
})
|
|
24
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
}, 'dsh-moa: status route')
|
|
28
|
+
|
|
29
|
+
// Route: /dsh-moa/models
|
|
30
|
+
|
|
31
|
+
ctx.effect(() => {
|
|
32
|
+
return ctx.webServer.register({
|
|
33
|
+
kind: 'exact',
|
|
34
|
+
path: '/dsh-moa/models',
|
|
35
|
+
handler: async (req, res) => {
|
|
36
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
37
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
38
|
+
if (_modelsCache.models.length > 0 && Date.now() - _modelsCache.timestamp < MODELS_CACHE_TTL) {
|
|
39
|
+
writeJson(res, 200, { ok: true, models: _modelsCache.models, cached: true })
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = []
|
|
44
|
+
const seen = new Set()
|
|
45
|
+
|
|
46
|
+
const add = (p, m) => {
|
|
47
|
+
if (!p || !m) return
|
|
48
|
+
const key = `${p}:${m}`
|
|
49
|
+
if (!seen.has(key)) {
|
|
50
|
+
seen.add(key)
|
|
51
|
+
result.push({ provider: p, model: m, label: key })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (ctx.llm) {
|
|
56
|
+
try {
|
|
57
|
+
if (typeof ctx.llm.listAvailableModels === 'function') {
|
|
58
|
+
const list = await ctx.llm.listAvailableModels()
|
|
59
|
+
for (const item of (list || [])) {
|
|
60
|
+
if (item?.provider && item?.id) add(item.provider, item.id)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
ctx.logger?.warn?.('[dsh-moa] ctx.llm.listAvailableModels error:', e)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
if (typeof ctx.llm.listConfigurableProviders === 'function') {
|
|
69
|
+
const cProvs = ctx.llm.listConfigurableProviders() || []
|
|
70
|
+
const queries = cProvs.map(async (cp) => {
|
|
71
|
+
const cpId = cp.provider || cp.id
|
|
72
|
+
if (!cpId || typeof ctx.llm.listModels !== 'function') return
|
|
73
|
+
try {
|
|
74
|
+
const mList = await Promise.race([
|
|
75
|
+
ctx.llm.listModels(cpId),
|
|
76
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000)),
|
|
77
|
+
])
|
|
78
|
+
for (const m of (mList || [])) {
|
|
79
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model || m?.name)
|
|
80
|
+
if (mId) add(cpId, mId)
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
/* provider query timeout or error handled best-effort */
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
await Promise.allSettled(queries)
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
ctx.logger?.warn?.('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Query settings for configured providers
|
|
94
|
+
try {
|
|
95
|
+
let s = null
|
|
96
|
+
try { s = ctx.get ? ctx.get('settings') : null } catch { s = null }
|
|
97
|
+
const getSetting = (k) => { try { return s?.get?.(k) } catch { return undefined } }
|
|
98
|
+
|
|
99
|
+
const def = getSetting('agent-default-model')
|
|
100
|
+
if (def?.provider && def?.model) add(def.provider, def.model)
|
|
101
|
+
|
|
102
|
+
const piAi = getSetting('llm-pi-ai')
|
|
103
|
+
for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
|
|
104
|
+
for (const m of (pData?.models || [])) {
|
|
105
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
106
|
+
if (mId) add(pName, mId)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sync = getSetting('dsh-model-sync')
|
|
111
|
+
for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
|
|
112
|
+
for (const m of (syncModels || [])) {
|
|
113
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model)
|
|
114
|
+
if (mId) add(syncProv, mId)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
ctx.logger?.warn?.('[dsh-moa] Error reading settings:', e)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_modelsCache = { timestamp: Date.now(), models: result }
|
|
122
|
+
writeJson(res, 200, { ok: true, models: result })
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}, 'dsh-moa: models route')
|
|
126
|
+
|
|
127
|
+
// Route: /dsh-moa/presets
|
|
128
|
+
|
|
129
|
+
ctx.effect(() => {
|
|
130
|
+
return ctx.webServer.register({
|
|
131
|
+
kind: 'exact',
|
|
132
|
+
path: '/dsh-moa/presets',
|
|
133
|
+
handler: async (req, res) => {
|
|
134
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
135
|
+
const cfg = live()
|
|
136
|
+
if (req.method === 'GET') {
|
|
137
|
+
writeJson(res, 200, {
|
|
138
|
+
ok: true,
|
|
139
|
+
defaultPreset: cfg.default_preset || 'default',
|
|
140
|
+
presets: cfg.presets || [],
|
|
141
|
+
})
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (req.method === 'POST' || req.method === 'PUT') {
|
|
146
|
+
if (!isSafeWriteRequest(req)) {
|
|
147
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const raw = await readBody(req)
|
|
152
|
+
const payload = JSON.parse(raw)
|
|
153
|
+
const newConfig = {
|
|
154
|
+
...cfg,
|
|
155
|
+
enabled: typeof payload.enabled === 'boolean' ? payload.enabled : cfg.enabled !== false,
|
|
156
|
+
default_preset: payload.default_preset || cfg.default_preset || 'default',
|
|
157
|
+
presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
|
|
158
|
+
...(payload.prices !== undefined ? { prices: payload.prices } : {}),
|
|
159
|
+
...(payload.smart_routing_enabled !== undefined ? { smart_routing_enabled: payload.smart_routing_enabled } : {}),
|
|
160
|
+
...(payload.smart_routing_model !== undefined ? { smart_routing_model: payload.smart_routing_model } : {}),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate the merged payload through the plugin schema before it
|
|
164
|
+
// reaches the settings storage: malformed payloads must be
|
|
165
|
+
// rejected with 400 instead of being persisted.
|
|
166
|
+
let validated
|
|
167
|
+
try {
|
|
168
|
+
validated = Config(structuredClone(newConfig))
|
|
169
|
+
} catch (verr) {
|
|
170
|
+
writeJson(res, 400, { ok: false, error: 'Invalid presets payload: ' + (verr?.message || String(verr)) })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Persist via saveConfig (in-memory and DSH profile settings)
|
|
175
|
+
let persistResult
|
|
176
|
+
if (typeof saveConfig === 'function') {
|
|
177
|
+
try { persistResult = await saveConfig(validated) }
|
|
178
|
+
catch (perr) { return writeJson(res, 500, { ok: false, error: 'Settings persistence failed: ' + (perr?.message || String(perr)) }) }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const unwrapped = typeof plainConfig === 'function' ? plainConfig(validated) : validated
|
|
182
|
+
writeJson(res, 200, {
|
|
183
|
+
ok: true,
|
|
184
|
+
persisted: persistResult?.persisted ?? false,
|
|
185
|
+
enabled: unwrapped.enabled !== false,
|
|
186
|
+
defaultPreset: unwrapped.default_preset,
|
|
187
|
+
presets: unwrapped.presets,
|
|
188
|
+
})
|
|
189
|
+
} catch (err) { writeJson(res, 400, { ok: false, error: err?.message || String(err) }) }
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
}, 'dsh-moa: presets route')
|
|
197
|
+
|
|
198
|
+
// Route: /dsh-moa/history (#9)
|
|
199
|
+
|
|
200
|
+
ctx.effect(() => {
|
|
201
|
+
return ctx.webServer.register({
|
|
202
|
+
kind: 'exact',
|
|
203
|
+
path: '/dsh-moa/route-preset',
|
|
204
|
+
handler: async (req, res) => {
|
|
205
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
206
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
207
|
+
if (!isSafeWriteRequest(req)) {
|
|
208
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const cfg = live()
|
|
213
|
+
if (cfg.enabled === false) {
|
|
214
|
+
writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const raw = await readBody(req)
|
|
218
|
+
const body = JSON.parse(raw || '{}')
|
|
219
|
+
const prompt = body?.prompt || ''
|
|
220
|
+
const activePreset = (cfg.presets || []).find((p) => p.name === (cfg.default_preset || 'default')) || cfg.presets?.[0]
|
|
221
|
+
const routingModel = (activePreset?.smart_routing_model?.model && activePreset.smart_routing_model)
|
|
222
|
+
|| (cfg.smart_routing_model?.model && cfg.smart_routing_model) || activePreset?.smart_routing_model || cfg.smart_routing_model
|
|
223
|
+
const result = await resolvePresetForPrompt({
|
|
224
|
+
prompt,
|
|
225
|
+
presets: cfg.presets || [],
|
|
226
|
+
defaultPreset: cfg.default_preset || 'default',
|
|
227
|
+
routingModel,
|
|
228
|
+
callLlm,
|
|
229
|
+
enabled: cfg.enabled !== false,
|
|
230
|
+
})
|
|
231
|
+
writeJson(res, 200, { ok: true, ...result })
|
|
232
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
}, 'dsh-moa: route preset endpoint')
|
|
236
|
+
|
|
237
|
+
let _lastRunTimestamp = 0
|
|
238
|
+
const RUN_MIN_INTERVAL_MS = 1000
|
|
239
|
+
|
|
240
|
+
// Route: /dsh-moa/run
|
|
241
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { getMoaHistory, getMoaRunById, isTestEnvironment } from '../history.js'
|
|
3
|
+
import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl } from '../file-workspace.js'
|
|
4
|
+
import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from '../access.js'
|
|
5
|
+
import { isSafeWriteRequest } from '../updater.js'
|
|
6
|
+
import { generateMoABenchmarkReport } from '../moa-report.js'
|
|
7
|
+
import { handleWorkbenchGet, handleWorkbenchAssemble } from '../moa-workbench.js'
|
|
8
|
+
|
|
9
|
+
export function registerWorkbenchRoutes(ctx, { live, callLlm, webServerPort, timeMachineEngine, workspaceRoot }) {
|
|
10
|
+
ctx.effect(() => {
|
|
11
|
+
return ctx.webServer.register({
|
|
12
|
+
kind: 'exact',
|
|
13
|
+
path: '/dsh-moa/promote',
|
|
14
|
+
handler: async (req, res) => {
|
|
15
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
16
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
17
|
+
if (!isSafeWriteRequest(req)) {
|
|
18
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readBody(req)
|
|
23
|
+
const body = JSON.parse(raw || '{}')
|
|
24
|
+
const candidateIndex = parseInt(body?.candidateIndex, 10)
|
|
25
|
+
if (isNaN(candidateIndex) || candidateIndex < 1) {
|
|
26
|
+
writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const runId = body?.runId
|
|
30
|
+
let recordedRun = null
|
|
31
|
+
if (runId) {
|
|
32
|
+
recordedRun = getMoaRunById(runId)
|
|
33
|
+
if (!recordedRun) {
|
|
34
|
+
writeJson(res, 404, { ok: false, error: `Run '${runId}' not found` })
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
recordedRun = getMoaHistory(1, 0)?.runs?.[0] || null
|
|
39
|
+
if (!recordedRun) {
|
|
40
|
+
writeJson(res, 400, { ok: false, error: 'No recorded MoA run found' })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (body?.cwd && path.resolve(body.cwd) !== path.resolve(recordedRun.cwd)) {
|
|
45
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cwd does not match recorded run cwd' })
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const effectiveCwd = recordedRun.cwd
|
|
49
|
+
const promotedFiles = await promoteCandidateWorkspace(effectiveCwd, candidateIndex, {
|
|
50
|
+
runId: recordedRun.id,
|
|
51
|
+
keepMoa: true,
|
|
52
|
+
port: ctx.webServer?.port,
|
|
53
|
+
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
54
|
+
force: Boolean(body?.force),
|
|
55
|
+
})
|
|
56
|
+
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
|
|
57
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}, 'dsh-moa: promote candidate route')
|
|
61
|
+
|
|
62
|
+
// Route: /dsh-moa/diff (Candidate Diff Viewer)
|
|
63
|
+
|
|
64
|
+
ctx.effect(() => {
|
|
65
|
+
return ctx.webServer.register({
|
|
66
|
+
kind: 'exact',
|
|
67
|
+
path: '/dsh-moa/diff',
|
|
68
|
+
handler: async (req, res) => {
|
|
69
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
70
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
71
|
+
try {
|
|
72
|
+
const u = new URL(req.url, 'http://localhost')
|
|
73
|
+
const rawRunId = u.searchParams.get('runId')
|
|
74
|
+
const queryCwd = u.searchParams.get('cwd')
|
|
75
|
+
let run = null
|
|
76
|
+
if (rawRunId && rawRunId !== 'latest' && rawRunId !== 'active') {
|
|
77
|
+
run = getMoaRunById(rawRunId)
|
|
78
|
+
if (!run) {
|
|
79
|
+
writeJson(res, 404, { ok: false, error: `Run '${rawRunId}' not found` })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
run = getMoaHistory(1, 0)?.runs?.[0] || null
|
|
84
|
+
if (!run) {
|
|
85
|
+
writeJson(res, 404, { ok: false, error: 'No recorded MoA run found' })
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (queryCwd && path.resolve(queryCwd) !== path.resolve(run.cwd)) {
|
|
90
|
+
writeJson(res, 403, { ok: false, error: 'Forbidden: cwd does not match recorded run cwd' })
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const runId = run.id
|
|
94
|
+
const cwd = run.cwd
|
|
95
|
+
const sanitizeTarget = (val, def) => {
|
|
96
|
+
const clean = String(val || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
|
|
97
|
+
return clean || def
|
|
98
|
+
}
|
|
99
|
+
const from = sanitizeTarget(u.searchParams.get('from'), '1')
|
|
100
|
+
const to = sanitizeTarget(u.searchParams.get('to'), 'curator-synthesis')
|
|
101
|
+
const rawFile = u.searchParams.get('file') || ''
|
|
102
|
+
const file = (rawFile && isSafeRelativePath(rawFile)) ? rawFile : ''
|
|
103
|
+
|
|
104
|
+
const fromFiles = await readCandidateFiles(cwd, from, { runId })
|
|
105
|
+
const toFiles = await readCandidateFiles(cwd, to, { runId })
|
|
106
|
+
|
|
107
|
+
// Collect all unique file paths
|
|
108
|
+
const allPaths = Array.from(new Set([
|
|
109
|
+
...fromFiles.map((f) => f.relativePath),
|
|
110
|
+
...toFiles.map((f) => f.relativePath),
|
|
111
|
+
])).sort()
|
|
112
|
+
|
|
113
|
+
const targetPath = file || allPaths[0] || ''
|
|
114
|
+
const fromItem = fromFiles.find((f) => f.relativePath === targetPath)
|
|
115
|
+
const toItem = toFiles.find((f) => f.relativePath === targetPath)
|
|
116
|
+
|
|
117
|
+
const diff = targetPath
|
|
118
|
+
? computeLineDiff(fromItem?.content || '', toItem?.content || '')
|
|
119
|
+
: []
|
|
120
|
+
|
|
121
|
+
writeJson(res, 200, {
|
|
122
|
+
ok: true,
|
|
123
|
+
from,
|
|
124
|
+
to,
|
|
125
|
+
files: allPaths,
|
|
126
|
+
selectedFile: targetPath,
|
|
127
|
+
fromSize: fromItem?.size || 0,
|
|
128
|
+
toSize: toItem?.size || 0,
|
|
129
|
+
diff,
|
|
130
|
+
})
|
|
131
|
+
} catch (err) { writeJson(res, 500, { ok: false, error: err?.message || String(err) }) }
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}, 'dsh-moa: candidate diff route')
|
|
135
|
+
|
|
136
|
+
// Route: /dsh-moa/history/export
|
|
137
|
+
|
|
138
|
+
ctx.effect(() => {
|
|
139
|
+
return ctx.webServer.register({
|
|
140
|
+
kind: 'prefix',
|
|
141
|
+
path: '/dsh-moa/runs',
|
|
142
|
+
handler: (req, res) => {
|
|
143
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
144
|
+
const urlPath = req.url.split('?')[0]
|
|
145
|
+
const parts = urlPath.split('/').filter(Boolean)
|
|
146
|
+
const runId = parts[2] || ''
|
|
147
|
+
if (parts[3] === 'workbench') {
|
|
148
|
+
const targetCwd = workspaceRoot || ctx?.reflect?.get?.('baseDir', false) || ctx?.reflect?.get?.('workspaceDir', false) || process.cwd()
|
|
149
|
+
if (req.method === 'GET') {
|
|
150
|
+
return handleWorkbenchGet(req, res, { runId, cwd: targetCwd })
|
|
151
|
+
}
|
|
152
|
+
if (req.method === 'POST') {
|
|
153
|
+
if (!isSafeWriteRequest(req)) {
|
|
154
|
+
return writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
155
|
+
}
|
|
156
|
+
return handleWorkbenchAssemble(req, res, { runId, cwd: targetCwd, live, callLlm, webServerPort, timeMachineEngine })
|
|
157
|
+
}
|
|
158
|
+
return writeJson(res, 405, { ok: false, error: 'Method not allowed' })
|
|
159
|
+
}
|
|
160
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
161
|
+
const isReport = parts[3] === 'report'
|
|
162
|
+
const run = runId ? getMoaRunById(runId) : null
|
|
163
|
+
if (!run) {
|
|
164
|
+
writeJson(res, 404, { ok: false, error: 'Run not found' })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (isReport) {
|
|
168
|
+
const u = new URL(req.url, 'http://127.0.0.1')
|
|
169
|
+
const format = u.searchParams.get('format') || 'md'
|
|
170
|
+
const report = run.benchmarkReport || generateMoABenchmarkReport({
|
|
171
|
+
runId: run.id,
|
|
172
|
+
timestamp: run.timestamp,
|
|
173
|
+
preset: run.preset,
|
|
174
|
+
prompt: run.prompt,
|
|
175
|
+
durationMs: run.durationMs,
|
|
176
|
+
usage: { totalTokens: run.totalTokens },
|
|
177
|
+
costUsd: run.totalCostUsd,
|
|
178
|
+
candidates: run.candidates,
|
|
179
|
+
winningIndex: run.winnerIndex,
|
|
180
|
+
winningLabel: run.winnerModel,
|
|
181
|
+
})
|
|
182
|
+
if (format === 'json') {
|
|
183
|
+
writeJson(res, 200, { ok: true, report: typeof report === 'object' ? (report.json || report) : report })
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
const mdText = typeof report === 'string' ? report : (report.markdown || JSON.stringify(report, null, 2))
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'text/markdown; charset=utf-8' })
|
|
188
|
+
res.end(mdText)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
writeJson(res, 200, { ok: true, run })
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
}, 'dsh-moa: run by id route')
|
|
195
|
+
|
|
196
|
+
// Route: /dsh-moa/route-preset
|
|
197
|
+
}
|