@goodandready/dsh-moa 0.2.53 → 0.2.55
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 +4 -1
- package/README.ru.md +3 -0
- package/lib/access.js +203 -0
- package/lib/routes.js +14 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,6 +302,9 @@ dsh-moa:
|
|
|
302
302
|
|
|
303
303
|
## 📊 REST API & Endpoints
|
|
304
304
|
|
|
305
|
+
> [!IMPORTANT]
|
|
306
|
+
> **Authentication & Access Control**: All HTTP endpoints enforce fail-closed authentication via the DeepSeek Harness Connection Service (`ctx.connection.requestRejection`). Unauthenticated remote callers over LAN/WAN receive HTTP 401 (`Authentication required`) or 403 (`Forbidden`), strictly preventing disclosure of prompts, workspace structures, and candidate files. Mutation endpoints additionally require loopback origin and CSRF validation (`sec-fetch-site`).
|
|
307
|
+
|
|
305
308
|
| Endpoint | Method | Description |
|
|
306
309
|
|:---|:---|:---|
|
|
307
310
|
| `/dsh-moa/status` | `GET` | Health/enablement snapshot used by the settings card status chip |
|
|
@@ -332,7 +335,7 @@ npm test
|
|
|
332
335
|
## 🛠️ Internal Tooling & Development
|
|
333
336
|
|
|
334
337
|
For local verification and package integrity validation:
|
|
335
|
-
- `npm test`: runs the full test suite (
|
|
338
|
+
- `npm test`: runs the full test suite (288 tests)
|
|
336
339
|
- `./deploy.sh`: local infrastructure validation script (verifies package size, identity parity in package.json/cordis.patch.yml/client.js, and tests). Excluded from the published npm package.
|
|
337
340
|
|
|
338
341
|
## 📄 License
|
package/README.ru.md
CHANGED
|
@@ -302,6 +302,9 @@ dsh-moa:
|
|
|
302
302
|
|
|
303
303
|
## 📊 REST API эндпоинты
|
|
304
304
|
|
|
305
|
+
> [!IMPORTANT]
|
|
306
|
+
> **Аутентификация и контроль доступа**: Все HTTP-эндпоинты плагина защищены каноническим сервисом соединений DeepSeek Harness (`ctx.connection.requestRejection`). Неаутентифицированные удалённые запросы из сети LAN/WAN отклоняются с кодом HTTP 401 (`Authentication required`) или 403 (`Forbidden`), предотвращая утечку промптов, структуры рабочей директории и файлов кандидатов. Мутирующие эндпоинты дополнительно требуют loopback-вызова и проверки CSRF (`sec-fetch-site`).
|
|
307
|
+
|
|
305
308
|
| Эндпоинт | Метод | Описание |
|
|
306
309
|
|:---|:---|:---|
|
|
307
310
|
| `/dsh-moa/status` | `GET` | Снимок здоровья/включенности для статус-бейджа карточки настроек |
|
package/lib/access.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Mixture of Agents (MoA) — Access Control & Route Guard (#257)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import { assertPathContained } from './file-workspace.js'
|
|
7
|
+
|
|
8
|
+
export function writeJson(res, status, body) {
|
|
9
|
+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
10
|
+
res.end(JSON.stringify(body))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let acc = ''
|
|
16
|
+
req.on('data', (chunk) => {
|
|
17
|
+
acc += chunk
|
|
18
|
+
if (acc.length > 5 * 1024 * 1024) { req.destroy(); reject(new Error('Payload Too Large')) }
|
|
19
|
+
})
|
|
20
|
+
req.on('end', () => resolve(acc))
|
|
21
|
+
req.on('error', reject)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAllowedWorkspace(targetCwd, allowedRoots) {
|
|
26
|
+
if (!targetCwd || typeof targetCwd !== 'string') return false
|
|
27
|
+
for (const root of allowedRoots) {
|
|
28
|
+
if (!root) continue
|
|
29
|
+
try {
|
|
30
|
+
assertPathContained(root, targetCwd)
|
|
31
|
+
return true
|
|
32
|
+
} catch {
|
|
33
|
+
// not within root
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getConnection(ctx) {
|
|
40
|
+
try {
|
|
41
|
+
const fromCtx = ctx?.reflect?.get
|
|
42
|
+
? (ctx.reflect.get('connection') || ctx?.connection)
|
|
43
|
+
: (ctx?.get ? (ctx.get('connection') || ctx?.connection) : Reflect.get(ctx || {}, 'connection'))
|
|
44
|
+
if (fromCtx) return fromCtx
|
|
45
|
+
if (ctx?.root) return getConnection(ctx.root)
|
|
46
|
+
return undefined
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isLoopbackIp(ip) {
|
|
53
|
+
if (!ip || typeof ip !== 'string') return false
|
|
54
|
+
const clean = ip.trim().toLowerCase().replace(/^::ffff:/, '').replace(/^\[|\]$/g, '')
|
|
55
|
+
return clean === '127.0.0.1' || clean === '::1' || clean === 'localhost'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function cookieNameForAuthority(authority) {
|
|
59
|
+
if (!authority || typeof authority !== 'string') return ''
|
|
60
|
+
const hash = crypto.createHash('sha256').update(authority.trim()).digest('base64url')
|
|
61
|
+
return 'dsh-auth-' + hash
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isLoopbackCookieName(name, host = '') {
|
|
65
|
+
if (!name || typeof name !== 'string' || !name.startsWith('dsh-auth-')) return false
|
|
66
|
+
const authorities = [
|
|
67
|
+
host,
|
|
68
|
+
'127.0.0.1:3000',
|
|
69
|
+
'127.0.0.1:3080',
|
|
70
|
+
'127.0.0.1:3082',
|
|
71
|
+
'127.0.0.1',
|
|
72
|
+
'localhost:3000',
|
|
73
|
+
'localhost:3080',
|
|
74
|
+
'localhost:3082',
|
|
75
|
+
'localhost',
|
|
76
|
+
'[::1]:3000',
|
|
77
|
+
'[::1]',
|
|
78
|
+
]
|
|
79
|
+
for (const auth of authorities) {
|
|
80
|
+
if (auth && isLoopbackIp(auth.split(':')[0])) {
|
|
81
|
+
if (cookieNameForAuthority(auth) === name) return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function hasClientCredentials(req) {
|
|
88
|
+
if (!req || !req.headers) return false
|
|
89
|
+
const cookie = req.headers.cookie
|
|
90
|
+
if (!cookie || typeof cookie !== 'string') return false
|
|
91
|
+
if (
|
|
92
|
+
cookie.includes('dsh_token=') ||
|
|
93
|
+
cookie.includes('dsh_lan_pin=') ||
|
|
94
|
+
cookie.includes('dsh_auth_session=')
|
|
95
|
+
) {
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const parts = cookie.split(';')
|
|
100
|
+
for (const part of parts) {
|
|
101
|
+
const trimmed = part.trim()
|
|
102
|
+
const eqIdx = trimmed.indexOf('=')
|
|
103
|
+
if (eqIdx === -1) continue
|
|
104
|
+
const name = trimmed.slice(0, eqIdx).trim()
|
|
105
|
+
if (name.startsWith('dsh-auth-')) {
|
|
106
|
+
if (!isLoopbackCookieName(name, req.headers.host)) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validates whether an incoming HTTP request is authenticated and admitted (#257).
|
|
116
|
+
*
|
|
117
|
+
* Requirements:
|
|
118
|
+
* 1. CSRF protection: rejects sec-fetch-site: cross-site with 403.
|
|
119
|
+
* 2. Reverse proxy inspection: if request is forwarded with non-loopback x-forwarded-for,
|
|
120
|
+
* verify that the client itself supplied authentic credentials (not just injected loopback proxy cookie).
|
|
121
|
+
* 3. Authoritative DSH connection gate: if connection.requestRejection is present,
|
|
122
|
+
* its verdict (401, 403, or undefined) is authoritative.
|
|
123
|
+
* 4. Network boundary: non-loopback callers without connection service authentication
|
|
124
|
+
* fail closed with 401. Loopback callers are admitted.
|
|
125
|
+
*
|
|
126
|
+
* @param {import('node:http').IncomingMessage} req
|
|
127
|
+
* @param {any} [ctx]
|
|
128
|
+
* @returns {number|undefined} Rejection status (401, 403) or undefined if admitted.
|
|
129
|
+
*/
|
|
130
|
+
export function getReadRejectionStatus(req, ctx = null) {
|
|
131
|
+
if (!req || !req.headers) return 403
|
|
132
|
+
|
|
133
|
+
// 1. Strict CSRF protection: reject cross-site requests
|
|
134
|
+
const site = req.headers['sec-fetch-site']
|
|
135
|
+
if (site === 'cross-site') {
|
|
136
|
+
return 403
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Reverse-proxy / forwarded client verification (#257)
|
|
140
|
+
const xff = req.headers['x-forwarded-for']
|
|
141
|
+
let isForwardedRemote = false
|
|
142
|
+
if (xff && typeof xff === 'string') {
|
|
143
|
+
const firstIp = xff.split(',')[0].trim()
|
|
144
|
+
if (firstIp && !isLoopbackIp(firstIp)) {
|
|
145
|
+
isForwardedRemote = true
|
|
146
|
+
if (!hasClientCredentials(req)) {
|
|
147
|
+
return 401
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Authoritative DSH connection service check (#257)
|
|
153
|
+
const conn = getConnection(ctx) || req?.connectionService || ctx?.connection
|
|
154
|
+
if (conn && typeof conn.requestRejection === 'function') {
|
|
155
|
+
let rejection
|
|
156
|
+
try {
|
|
157
|
+
rejection = conn.requestRejection(req)
|
|
158
|
+
} catch {
|
|
159
|
+
rejection = 403
|
|
160
|
+
}
|
|
161
|
+
if (rejection !== undefined) {
|
|
162
|
+
if (typeof rejection === 'number' && Number.isInteger(rejection) && rejection >= 400 && rejection < 600) {
|
|
163
|
+
return rejection
|
|
164
|
+
}
|
|
165
|
+
return rejection === 401 ? 401 : 403
|
|
166
|
+
}
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. Network boundary evaluation when connection service is absent
|
|
171
|
+
const rawRemote = req?.socket?.remoteAddress || req?.connection?.remoteAddress || ''
|
|
172
|
+
const cleanRemote = String(rawRemote).trim().toLowerCase().replace(/^::ffff:/, '')
|
|
173
|
+
|
|
174
|
+
if (cleanRemote && !isLoopbackIp(cleanRemote)) {
|
|
175
|
+
return 401
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isForwardedRemote) {
|
|
179
|
+
return 401
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const host = String(req.headers.host || '').trim().toLowerCase()
|
|
183
|
+
if (!cleanRemote && host) {
|
|
184
|
+
const hostName = host.split(':')[0]
|
|
185
|
+
if (!isLoopbackIp(hostName)) {
|
|
186
|
+
return 401
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return undefined
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function assertAuthorizedRequest(req, res, ctx = null) {
|
|
194
|
+
const status = getReadRejectionStatus(req, ctx)
|
|
195
|
+
if (status !== undefined) {
|
|
196
|
+
writeJson(res, status, {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: status === 401 ? 'Authentication required' : 'Forbidden',
|
|
199
|
+
})
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
return true
|
|
203
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import os from 'node:os'
|
|
7
7
|
import { getMoaHistory, getMoaLeaderboard, getMoaRunById, exportMoaHistory, isTestEnvironment } from './history.js'
|
|
8
|
-
import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl
|
|
8
|
+
import { promoteCandidateWorkspace, readCandidateFiles, computeLineDiff, isSafeRelativePath, isLoopbackUrl } from './file-workspace.js'
|
|
9
|
+
import { writeJson, readBody, isAllowedWorkspace, assertAuthorizedRequest } from './access.js'
|
|
9
10
|
import { isSafeWriteRequest } from './updater.js'
|
|
10
11
|
import { runMoAPipeline } from './moa-runner.js'
|
|
11
12
|
import { generateMoABenchmarkReport } from './moa-report.js'
|
|
@@ -14,37 +15,6 @@ import { resolvePresetForPrompt } from './moa-router.js'
|
|
|
14
15
|
let _modelsCache = { timestamp: 0, models: [] }
|
|
15
16
|
const MODELS_CACHE_TTL = 60 * 1000
|
|
16
17
|
|
|
17
|
-
function writeJson(res, status, body) {
|
|
18
|
-
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
19
|
-
res.end(JSON.stringify(body))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function readBody(req) {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
let acc = ''
|
|
25
|
-
req.on('data', (chunk) => {
|
|
26
|
-
acc += chunk
|
|
27
|
-
if (acc.length > 5 * 1024 * 1024) { req.destroy(); reject(new Error('Payload Too Large')) }
|
|
28
|
-
})
|
|
29
|
-
req.on('end', () => resolve(acc))
|
|
30
|
-
req.on('error', reject)
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
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
18
|
export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine, workspaceRoot }) {
|
|
49
19
|
|
|
50
20
|
ctx.effect(() => {
|
|
@@ -52,6 +22,7 @@ ctx.effect(() => {
|
|
|
52
22
|
kind: 'exact',
|
|
53
23
|
path: '/dsh-moa/status',
|
|
54
24
|
handler: (req, res) => {
|
|
25
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
55
26
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
56
27
|
try {
|
|
57
28
|
const cfg = live()
|
|
@@ -72,6 +43,7 @@ ctx.effect(() => {
|
|
|
72
43
|
kind: 'exact',
|
|
73
44
|
path: '/dsh-moa/models',
|
|
74
45
|
handler: async (req, res) => {
|
|
46
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
75
47
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
76
48
|
if (_modelsCache.models.length > 0 && Date.now() - _modelsCache.timestamp < MODELS_CACHE_TTL) {
|
|
77
49
|
writeJson(res, 200, { ok: true, models: _modelsCache.models, cached: true })
|
|
@@ -168,6 +140,7 @@ ctx.effect(() => {
|
|
|
168
140
|
kind: 'exact',
|
|
169
141
|
path: '/dsh-moa/presets',
|
|
170
142
|
handler: async (req, res) => {
|
|
143
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
171
144
|
const cfg = live()
|
|
172
145
|
if (req.method === 'GET') {
|
|
173
146
|
writeJson(res, 200, {
|
|
@@ -237,6 +210,7 @@ ctx.effect(() => {
|
|
|
237
210
|
kind: 'exact',
|
|
238
211
|
path: '/dsh-moa/history',
|
|
239
212
|
handler: async (req, res) => {
|
|
213
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
240
214
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
241
215
|
try {
|
|
242
216
|
const u = new URL(req.url, 'http://localhost')
|
|
@@ -255,6 +229,7 @@ ctx.effect(() => {
|
|
|
255
229
|
kind: 'exact',
|
|
256
230
|
path: '/dsh-moa/leaderboard',
|
|
257
231
|
handler: async (req, res) => {
|
|
232
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
258
233
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
259
234
|
try {
|
|
260
235
|
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
@@ -284,6 +259,7 @@ ctx.effect(() => {
|
|
|
284
259
|
kind: 'exact',
|
|
285
260
|
path: '/dsh-moa/promote',
|
|
286
261
|
handler: async (req, res) => {
|
|
262
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
287
263
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
288
264
|
if (!isSafeWriteRequest(req)) {
|
|
289
265
|
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
@@ -336,6 +312,7 @@ ctx.effect(() => {
|
|
|
336
312
|
kind: 'exact',
|
|
337
313
|
path: '/dsh-moa/diff',
|
|
338
314
|
handler: async (req, res) => {
|
|
315
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
339
316
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
340
317
|
try {
|
|
341
318
|
const u = new URL(req.url, 'http://localhost')
|
|
@@ -408,6 +385,7 @@ ctx.effect(() => {
|
|
|
408
385
|
kind: 'exact',
|
|
409
386
|
path: '/dsh-moa/history/export',
|
|
410
387
|
handler: async (req, res) => {
|
|
388
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
411
389
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
412
390
|
try {
|
|
413
391
|
const url = new URL(req.url || '', 'http://127.0.0.1')
|
|
@@ -430,8 +408,9 @@ ctx.effect(() => {
|
|
|
430
408
|
ctx.effect(() => {
|
|
431
409
|
return ctx.webServer.register({
|
|
432
410
|
kind: 'prefix',
|
|
433
|
-
path: '/dsh-moa/runs
|
|
411
|
+
path: '/dsh-moa/runs',
|
|
434
412
|
handler: (req, res) => {
|
|
413
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
435
414
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET required' })
|
|
436
415
|
const urlPath = req.url.split('?')[0]
|
|
437
416
|
const parts = urlPath.split('/').filter(Boolean)
|
|
@@ -477,6 +456,7 @@ ctx.effect(() => {
|
|
|
477
456
|
kind: 'exact',
|
|
478
457
|
path: '/dsh-moa/route-preset',
|
|
479
458
|
handler: async (req, res) => {
|
|
459
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
480
460
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
481
461
|
if (!isSafeWriteRequest(req)) {
|
|
482
462
|
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|
|
@@ -517,6 +497,7 @@ ctx.effect(() => {
|
|
|
517
497
|
kind: 'exact',
|
|
518
498
|
path: '/dsh-moa/run',
|
|
519
499
|
handler: async (req, res) => {
|
|
500
|
+
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
520
501
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST required' })
|
|
521
502
|
if (!isSafeWriteRequest(req)) {
|
|
522
503
|
writeJson(res, 403, { ok: false, error: 'Forbidden: cross-site request rejected' })
|