openclaw-node-harness 2.1.0 → 2.2.0
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/bin/lane-watchdog.js +54 -23
- package/bin/mesh-agent.js +49 -18
- package/bin/mesh-bridge.js +3 -2
- package/bin/mesh-deploy.js +4 -0
- package/bin/mesh-health-publisher.js +41 -1
- package/bin/mesh-task-daemon.js +14 -4
- package/bin/mesh.js +17 -43
- package/install.sh +3 -2
- package/lib/agent-activity.js +2 -2
- package/lib/exec-safety.js +163 -0
- package/lib/kanban-io.js +20 -33
- package/lib/llm-providers.js +27 -0
- package/lib/mcp-knowledge/core.mjs +7 -5
- package/lib/mcp-knowledge/server.mjs +8 -1
- package/lib/mesh-collab.js +274 -250
- package/lib/mesh-harness.js +6 -0
- package/lib/mesh-plans.js +84 -45
- package/lib/mesh-tasks.js +113 -81
- package/lib/nats-resolve.js +4 -4
- package/lib/pre-compression-flush.mjs +2 -0
- package/lib/session-store.mjs +6 -3
- package/mission-control/package-lock.json +4188 -3698
- package/mission-control/package.json +2 -2
- package/mission-control/src/app/api/diagnostics/route.ts +8 -0
- package/mission-control/src/app/api/diagnostics/test-runner/route.ts +8 -0
- package/mission-control/src/app/api/memory/graph/route.ts +34 -18
- package/mission-control/src/app/api/memory/search/route.ts +9 -5
- package/mission-control/src/app/api/mesh/identity/route.ts +13 -5
- package/mission-control/src/app/api/mesh/nodes/route.ts +8 -0
- package/mission-control/src/app/api/settings/gateway/route.ts +62 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +49 -12
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +7 -1
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +24 -5
- package/mission-control/src/app/api/souls/route.ts +6 -4
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +7 -1
- package/mission-control/src/app/api/tasks/[id]/route.ts +20 -4
- package/mission-control/src/app/api/tasks/route.ts +68 -9
- package/mission-control/src/app/api/workspace/read/route.ts +11 -0
- package/mission-control/src/lib/config.ts +11 -2
- package/mission-control/src/lib/db/index.ts +16 -1
- package/mission-control/src/lib/memory/extract.ts +2 -1
- package/mission-control/src/lib/memory/retrieval.ts +3 -2
- package/mission-control/src/lib/sync/tasks.ts +4 -1
- package/mission-control/src/middleware.ts +82 -0
- package/package.json +1 -1
- package/services/launchd/ai.openclaw.lane-watchdog.plist +1 -1
- package/services/launchd/ai.openclaw.log-rotate.plist +11 -0
- package/services/launchd/ai.openclaw.mesh-agent.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +4 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +4 -0
- package/services/launchd/ai.openclaw.mission-control.plist +5 -4
- package/uninstall.sh +37 -9
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exec-safety.js — Shared command safety filtering for mesh exec.
|
|
3
|
+
*
|
|
4
|
+
* Used by both CLI-side (mesh.js) and server-side (NATS exec handler)
|
|
5
|
+
* to block destructive or unauthorized commands before execution.
|
|
6
|
+
*
|
|
7
|
+
* Two layers:
|
|
8
|
+
* 1. DESTRUCTIVE_PATTERNS — blocklist of known-dangerous patterns
|
|
9
|
+
* 2. ALLOWED_PREFIXES — allowlist for server-side execution (opt-in)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// Shell metacharacter detection — blocks command chaining/injection.
|
|
15
|
+
// Safe pipes to common read-only utilities are allowed.
|
|
16
|
+
const SHELL_CHAIN_PATTERNS = /[\n\r\0;`]|\$\(|\|\||&&|<\(|>\(|<<|>>|>\s|\|(?!\s*grep\b|\s*head\b|\s*tail\b|\s*wc\b|\s*sort\b)/;
|
|
17
|
+
|
|
18
|
+
function containsShellChaining(cmd) {
|
|
19
|
+
// Allow safe pipes to common read-only utilities
|
|
20
|
+
return SHELL_CHAIN_PATTERNS.test(cmd);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Dangerous flags that allow arbitrary code execution via node
|
|
24
|
+
const DANGEROUS_NODE_FLAGS = /\bnode\s+(-e\b|--eval\b|-p\b|--print\b|-r\b|--require\b|--import\b|--loader\b|--experimental-loader\b)/;
|
|
25
|
+
|
|
26
|
+
// Dangerous git flags that allow arbitrary config / code execution
|
|
27
|
+
const DANGEROUS_GIT_FLAGS = /\bgit\s+(-c\s|--config\s)/;
|
|
28
|
+
|
|
29
|
+
// Dangerous find flags that allow arbitrary command execution
|
|
30
|
+
const DANGEROUS_FIND_FLAGS = /\bfind\b.*\s(-exec\b|-execdir\b|-delete\b|-ok\b|-okdir\b)/;
|
|
31
|
+
|
|
32
|
+
// Dangerous make variable overrides (SHELL=, CC=, etc.)
|
|
33
|
+
const DANGEROUS_MAKE_FLAGS = /\bmake\b.*\b(SHELL|CC|CXX|LD|AR)=/;
|
|
34
|
+
|
|
35
|
+
// Dangerous python flags that allow arbitrary code execution
|
|
36
|
+
const DANGEROUS_PYTHON_FLAGS = /\bpython3?\s+(-c\b|-m\s+http)/;
|
|
37
|
+
|
|
38
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
39
|
+
/\brm\s+(-[a-zA-Z]*)?r[a-zA-Z]*f/, // rm -rf, rm -fr, rm --recursive --force
|
|
40
|
+
/\brm\s+(-[a-zA-Z]*)?f[a-zA-Z]*r/, // rm -fr variants
|
|
41
|
+
/\bmkfs\b/, // format filesystem
|
|
42
|
+
/\bdd\s+.*of=/, // raw disk write
|
|
43
|
+
/\b>\s*\/dev\/[sh]d/, // write to raw device
|
|
44
|
+
/\bcurl\b.*\|\s*(ba)?sh/, // curl pipe to shell
|
|
45
|
+
/\bwget\b.*\|\s*(ba)?sh/, // wget pipe to shell
|
|
46
|
+
/\bchmod\s+(-[a-zA-Z]*\s+)?777\s+\//, // chmod 777 on root paths
|
|
47
|
+
/\b:(){ :\|:& };:/, // fork bomb
|
|
48
|
+
/\bsudo\b/, // sudo escalation
|
|
49
|
+
/\bsu\s+-?\s/, // su user switch
|
|
50
|
+
/\bpasswd\b/, // password change
|
|
51
|
+
/\buseradd\b|\buserdel\b/, // user management
|
|
52
|
+
/\biptables\b|\bnft\b/, // firewall modification
|
|
53
|
+
/\bsystemctl\s+(stop|disable|mask)/, // service disruption
|
|
54
|
+
/\blaunchctl\s+(unload|remove)/, // macOS service disruption
|
|
55
|
+
/\bkill\s+-9\s+1\b/, // kill init/launchd
|
|
56
|
+
/>\s*\/etc\//, // overwrite system config
|
|
57
|
+
/\beval\b.*\$\(/, // eval with command substitution
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Allowed command prefixes for server-side NATS exec.
|
|
62
|
+
* Only commands starting with one of these are permitted.
|
|
63
|
+
* CLI-side uses blocklist only; server-side uses both blocklist + allowlist.
|
|
64
|
+
*/
|
|
65
|
+
const ALLOWED_EXEC_PREFIXES = [
|
|
66
|
+
'git ', 'node ', 'python ', 'python3 ',
|
|
67
|
+
'npm test', 'npm run test', 'npm run lint', 'npm run build', 'npm run dev',
|
|
68
|
+
'npm run start', 'npm install', 'npm ci', 'npm ls', 'npm outdated',
|
|
69
|
+
'npm audit', 'npm version', 'npm pack', 'npm run check',
|
|
70
|
+
'npx vitest', 'npx jest', 'npx eslint', 'npx prettier', 'npx tsc',
|
|
71
|
+
'cat ', 'ls ', 'head ', 'tail ', 'grep ', 'find ', 'wc ',
|
|
72
|
+
'echo ', 'date ', 'uptime ', 'df ', 'free ', 'ps ',
|
|
73
|
+
'bash openclaw/', 'bash ~/openclaw/', 'bash ./bin/',
|
|
74
|
+
'pwd', 'which ',
|
|
75
|
+
'cargo ', 'go ', 'make ', 'pytest ', 'jest ', 'vitest ',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a command matches any destructive pattern.
|
|
80
|
+
* @param {string} command
|
|
81
|
+
* @returns {{ blocked: boolean, pattern?: RegExp }}
|
|
82
|
+
*/
|
|
83
|
+
function checkDestructivePatterns(command) {
|
|
84
|
+
for (const pattern of DESTRUCTIVE_PATTERNS) {
|
|
85
|
+
if (pattern.test(command)) {
|
|
86
|
+
return { blocked: true, pattern };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { blocked: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if a command is allowed by the server-side allowlist.
|
|
94
|
+
* @param {string} command
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
function isAllowedExecCommand(command) {
|
|
98
|
+
const trimmed = (command || '').trim();
|
|
99
|
+
if (!trimmed) return false;
|
|
100
|
+
return ALLOWED_EXEC_PREFIXES.some(p => trimmed.startsWith(p));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Full server-side validation: blocklist + allowlist.
|
|
105
|
+
* Returns { allowed: true } or { allowed: false, reason: string }.
|
|
106
|
+
* @param {string} command
|
|
107
|
+
* @returns {{ allowed: boolean, reason?: string }}
|
|
108
|
+
*/
|
|
109
|
+
function validateExecCommand(command) {
|
|
110
|
+
const trimmed = (command || '').trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return { allowed: false, reason: 'Empty command' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (containsShellChaining(trimmed)) {
|
|
116
|
+
return { allowed: false, reason: `Command contains shell chaining operators: ${trimmed.slice(0, 80)}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (DANGEROUS_NODE_FLAGS.test(trimmed)) {
|
|
120
|
+
return { allowed: false, reason: `Dangerous node flag detected: ${trimmed.slice(0, 80)}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (DANGEROUS_GIT_FLAGS.test(trimmed)) {
|
|
124
|
+
return { allowed: false, reason: `Dangerous git flag detected: ${trimmed.slice(0, 80)}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (DANGEROUS_FIND_FLAGS.test(trimmed)) {
|
|
128
|
+
return { allowed: false, reason: `Dangerous find flag detected: ${trimmed.slice(0, 80)}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (DANGEROUS_MAKE_FLAGS.test(trimmed)) {
|
|
132
|
+
return { allowed: false, reason: `Dangerous make variable override detected: ${trimmed.slice(0, 80)}` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (DANGEROUS_PYTHON_FLAGS.test(trimmed)) {
|
|
136
|
+
return { allowed: false, reason: `Dangerous python flag detected: ${trimmed.slice(0, 80)}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const destructive = checkDestructivePatterns(trimmed);
|
|
140
|
+
if (destructive.blocked) {
|
|
141
|
+
return { allowed: false, reason: `Blocked by destructive pattern: ${destructive.pattern}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!isAllowedExecCommand(trimmed)) {
|
|
145
|
+
return { allowed: false, reason: `Command not in server-side allowlist: ${trimmed.slice(0, 80)}` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { allowed: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
DESTRUCTIVE_PATTERNS,
|
|
153
|
+
ALLOWED_EXEC_PREFIXES,
|
|
154
|
+
DANGEROUS_NODE_FLAGS,
|
|
155
|
+
DANGEROUS_GIT_FLAGS,
|
|
156
|
+
DANGEROUS_FIND_FLAGS,
|
|
157
|
+
DANGEROUS_MAKE_FLAGS,
|
|
158
|
+
DANGEROUS_PYTHON_FLAGS,
|
|
159
|
+
checkDestructivePatterns,
|
|
160
|
+
isAllowedExecCommand,
|
|
161
|
+
validateExecCommand,
|
|
162
|
+
containsShellChaining,
|
|
163
|
+
};
|
package/lib/kanban-io.js
CHANGED
|
@@ -28,47 +28,30 @@ const ACTIVE_TASKS_PATH = path.join(
|
|
|
28
28
|
// Prevents lost updates when mesh-bridge and memory-daemon write concurrently.
|
|
29
29
|
// See architecture note above for why this is local-only.
|
|
30
30
|
|
|
31
|
-
function withMkdirLock(filePath, fn) {
|
|
31
|
+
async function withMkdirLock(filePath, fn) {
|
|
32
32
|
const lockDir = filePath + '.lk';
|
|
33
|
-
const maxWait = 5000;
|
|
33
|
+
const maxWait = 5000;
|
|
34
|
+
const pollInterval = 50;
|
|
34
35
|
const start = Date.now();
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
while (true) {
|
|
37
|
+
while (Date.now() - start < maxWait) {
|
|
38
38
|
try {
|
|
39
39
|
fs.mkdirSync(lockDir);
|
|
40
|
-
break; // got the lock
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (err.code !== 'EEXIST') throw err;
|
|
43
|
-
// Lock held by another process — check for stale lock (>30s)
|
|
44
40
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
fs.rmdirSync(lockDir);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
} catch { /* stat failed, lock was just released */ continue; }
|
|
52
|
-
|
|
53
|
-
if (Date.now() - start > maxWait) {
|
|
54
|
-
throw new Error(`kanban-io: lock timeout after ${maxWait}ms on ${filePath}`);
|
|
41
|
+
return await fn();
|
|
42
|
+
} finally {
|
|
43
|
+
try { fs.rmdirSync(lockDir); } catch {}
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} catch {
|
|
61
|
-
const end = Date.now() + 10;
|
|
62
|
-
while (Date.now() < end) { /* busy-wait fallback */ }
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 'EEXIST') {
|
|
47
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
48
|
+
continue;
|
|
63
49
|
}
|
|
50
|
+
throw err;
|
|
64
51
|
}
|
|
65
52
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return fn();
|
|
69
|
-
} finally {
|
|
70
|
-
try { fs.rmdirSync(lockDir); } catch { /* already released */ }
|
|
71
|
-
}
|
|
53
|
+
// Timeout — refuse to proceed without lock to prevent data corruption
|
|
54
|
+
throw new Error(`kanban-io: lock acquisition timeout after ${maxWait}ms — file may be corrupted`);
|
|
72
55
|
}
|
|
73
56
|
|
|
74
57
|
// ── Parser ──────────────────────────────────────────
|
|
@@ -367,9 +350,13 @@ function _updateTaskInPlaceUnsafe(filePath, taskId, fieldUpdates = {}, arrayAppe
|
|
|
367
350
|
...lines.slice(blockEnd),
|
|
368
351
|
];
|
|
369
352
|
|
|
370
|
-
// Atomic write
|
|
353
|
+
// Atomic write — fsync before rename to ensure data hits disk
|
|
371
354
|
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
372
|
-
|
|
355
|
+
const output = newLines.join('\n');
|
|
356
|
+
const fd = fs.openSync(tmpPath, 'w');
|
|
357
|
+
fs.writeSync(fd, output);
|
|
358
|
+
fs.fsyncSync(fd);
|
|
359
|
+
fs.closeSync(fd);
|
|
373
360
|
fs.renameSync(tmpPath, filePath);
|
|
374
361
|
}
|
|
375
362
|
|
package/lib/llm-providers.js
CHANGED
|
@@ -21,6 +21,30 @@ const path = require('path');
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const os = require('os');
|
|
23
23
|
|
|
24
|
+
// ── Shell Command Security ─────────────────────────
|
|
25
|
+
const SHELL_CHAIN_PATTERNS = /[\n\r\0;`]|\$\(|\|\||&&|<\(|>\(|<<|>>|>\s|\|(?!\s*grep\b|\s*head\b|\s*tail\b|\s*wc\b|\s*sort\b)/;
|
|
26
|
+
|
|
27
|
+
const DANGEROUS_FIND_FLAGS = /\bfind\b.*\s(-exec\b|-execdir\b|-delete\b|-ok\b|-okdir\b)/;
|
|
28
|
+
|
|
29
|
+
const DANGEROUS_NODE_FLAGS = /\bnode\s+(-e\b|--eval\b|-p\b|--print\b|-r\b|--require\b|--import\b|--loader\b|--experimental-loader\b)/;
|
|
30
|
+
|
|
31
|
+
const SHELL_PROVIDER_ALLOWED_PREFIXES = [
|
|
32
|
+
'npm test', 'npm run', 'node ', 'python ', 'pytest', 'cargo test',
|
|
33
|
+
'go test', 'make', 'jest', 'vitest', 'mocha',
|
|
34
|
+
'bash openclaw/', 'bash ~/openclaw/', 'bash ./bin/',
|
|
35
|
+
'sh openclaw/', 'sh ~/openclaw/', 'sh ./bin/',
|
|
36
|
+
'cat ', 'echo ', 'ls ', 'grep ', 'find ', 'git '
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function validateShellCommand(cmd) {
|
|
40
|
+
const trimmed = (cmd || '').trim();
|
|
41
|
+
if (!trimmed) return false;
|
|
42
|
+
if (SHELL_CHAIN_PATTERNS.test(trimmed)) return false;
|
|
43
|
+
if (DANGEROUS_NODE_FLAGS.test(trimmed)) return false;
|
|
44
|
+
if (DANGEROUS_FIND_FLAGS.test(trimmed)) return false;
|
|
45
|
+
return SHELL_PROVIDER_ALLOWED_PREFIXES.some(p => trimmed.startsWith(p));
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
// ── Generic Provider Factory ────────────────────────
|
|
25
49
|
// Most agentic coding CLIs follow a similar pattern:
|
|
26
50
|
// binary [prompt-flag] "prompt" [model-flag] model [cwd-flag] dir
|
|
@@ -167,6 +191,9 @@ const PROVIDERS = {
|
|
|
167
191
|
buildArgs(prompt, model, task) {
|
|
168
192
|
// Use task.description (the raw command) if available, fall back to prompt
|
|
169
193
|
const cmd = (task && task.description) ? task.description : prompt;
|
|
194
|
+
if (!validateShellCommand(cmd)) {
|
|
195
|
+
throw new Error(`Shell provider: command blocked by security filter: ${cmd.slice(0, 80)}`);
|
|
196
|
+
}
|
|
170
197
|
return ['-c', cmd];
|
|
171
198
|
},
|
|
172
199
|
cleanEnv(env) {
|
|
@@ -349,14 +349,14 @@ export async function indexWorkspace(db, root, opts = {}) {
|
|
|
349
349
|
const texts = chunks.map(c => c.text);
|
|
350
350
|
const vectors = await embedBatch(texts);
|
|
351
351
|
|
|
352
|
-
//
|
|
352
|
+
// Insert chunks and their vector embeddings
|
|
353
353
|
const doInsert = db.transaction(() => {
|
|
354
354
|
insertDoc.run(file.rel, hash, Date.now(), chunks.length);
|
|
355
355
|
for (let i = 0; i < chunks.length; i++) {
|
|
356
356
|
const snippet = chunks[i].text.slice(0, SNIPPET_LENGTH).replace(/\n/g, ' ');
|
|
357
357
|
const info = insertChunk.run(file.rel, chunks[i].section, chunks[i].text, snippet);
|
|
358
358
|
const vecBuf = Buffer.from(vectors[i].buffer);
|
|
359
|
-
db.prepare(`INSERT INTO chunk_vectors VALUES (
|
|
359
|
+
db.prepare(`INSERT INTO chunk_vectors VALUES (?, ?)`).run(info.lastInsertRowid, vecBuf);
|
|
360
360
|
}
|
|
361
361
|
});
|
|
362
362
|
doInsert();
|
|
@@ -373,6 +373,7 @@ export async function indexWorkspace(db, root, opts = {}) {
|
|
|
373
373
|
// ─── Search Functions ────────────────────────────────────────────────────────
|
|
374
374
|
|
|
375
375
|
export async function semanticSearch(db, query, limit = 10) {
|
|
376
|
+
const safeLimit = Math.max(1, Math.min(100, Math.floor(Number(limit) || 10)));
|
|
376
377
|
const count = db.prepare('SELECT COUNT(*) as c FROM chunk_vectors').get().c;
|
|
377
378
|
if (count === 0) return [];
|
|
378
379
|
|
|
@@ -388,7 +389,7 @@ export async function semanticSearch(db, query, limit = 10) {
|
|
|
388
389
|
c.snippet
|
|
389
390
|
FROM chunk_vectors cv
|
|
390
391
|
JOIN chunks c ON c.id = cv.rowid
|
|
391
|
-
WHERE embedding MATCH ? AND k = ${
|
|
392
|
+
WHERE embedding MATCH ? AND k = ${safeLimit}
|
|
392
393
|
ORDER BY distance
|
|
393
394
|
`).all(vecBuf);
|
|
394
395
|
|
|
@@ -401,6 +402,7 @@ export async function semanticSearch(db, query, limit = 10) {
|
|
|
401
402
|
}
|
|
402
403
|
|
|
403
404
|
export async function findRelated(db, docPath, limit = 10) {
|
|
405
|
+
const safeLimit = Math.max(1, Math.min(100, Math.floor(Number(limit) || 10)));
|
|
404
406
|
const chunkIds = db.prepare('SELECT id FROM chunks WHERE doc_path = ?').all(docPath);
|
|
405
407
|
|
|
406
408
|
if (chunkIds.length === 0) {
|
|
@@ -445,7 +447,7 @@ export async function findRelated(db, docPath, limit = 10) {
|
|
|
445
447
|
c.snippet
|
|
446
448
|
FROM chunk_vectors cv
|
|
447
449
|
JOIN chunks c ON c.id = cv.rowid
|
|
448
|
-
WHERE embedding MATCH ? AND k = ${
|
|
450
|
+
WHERE embedding MATCH ? AND k = ${safeLimit * 3}
|
|
449
451
|
ORDER BY distance
|
|
450
452
|
`).all(vecBuf);
|
|
451
453
|
|
|
@@ -459,7 +461,7 @@ export async function findRelated(db, docPath, limit = 10) {
|
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
463
|
|
|
462
|
-
return [...seen.values()].slice(0,
|
|
464
|
+
return [...seen.values()].slice(0, safeLimit).map(r => ({
|
|
463
465
|
path: r.doc_path,
|
|
464
466
|
section: r.section,
|
|
465
467
|
score: parseFloat((1 - r.distance * r.distance / 2).toFixed(4)),
|
|
@@ -163,7 +163,14 @@ async function startHttp(engine, port, host) {
|
|
|
163
163
|
// Parse body
|
|
164
164
|
const chunks = [];
|
|
165
165
|
for await (const chunk of req) chunks.push(chunk);
|
|
166
|
-
|
|
166
|
+
let body;
|
|
167
|
+
try {
|
|
168
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
169
|
+
} catch (e) {
|
|
170
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
171
|
+
res.end(JSON.stringify({ error: 'Invalid JSON in request body' }));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
167
174
|
|
|
168
175
|
await transport.handleRequest(req, res, body);
|
|
169
176
|
return;
|