pi-mega-compact 0.8.24 → 0.8.26
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 +26 -0
- package/dist/extensions/mega-compact-s38.test.js +263 -14
- package/dist/extensions/mega-compact.js +15 -0
- package/dist/extensions/mega-config.js +3 -0
- package/dist/extensions/mega-events/agent-handlers.js +211 -26
- package/dist/extensions/mega-events/context-handler.js +45 -7
- package/dist/extensions/mega-events/error-classifier.js +125 -18
- package/dist/extensions/mega-pipeline/compact.js +24 -13
- package/dist/extensions/mega-pipeline/recall.js +31 -2
- package/dist/extensions/mega-runtime/dashboard-snapshot.js +4 -0
- package/dist/extensions/mega-runtime/runtime-snapshot.js +4 -0
- package/dist/extensions/mega-runtime/runtime.js +58 -5
- package/dist/src/boundary.js +79 -43
- package/dist/src/boundary.test.js +119 -2
- package/dist/src/canary.js +10 -0
- package/dist/src/config/dedup.js +14 -0
- package/dist/src/config.js +3 -1
- package/dist/src/dedup/raptor/buildHistory.js +164 -0
- package/dist/src/dedup/raptor/buildHistory.test.js +292 -0
- package/dist/src/dedup/raptor/index.js +38 -0
- package/dist/src/dedup/raptor/multilevel-serve.test.js +229 -0
- package/dist/src/dedup/raptor/multilevel.js +17 -5
- package/dist/src/dedup/raptor/multilevel.test.js +36 -1
- package/dist/src/dedup/raptor/raptor.test.js +43 -0
- package/dist/src/dedup/raptor/retrieval.js +14 -2
- package/dist/src/dedup/raptor/retrieval.test.js +95 -0
- package/dist/src/dedup/raptor/serve-gate.test.js +298 -0
- package/dist/src/dedup/raptor/summarizer.js +1 -0
- package/dist/src/dedup/raptor/tree.js +16 -2
- package/dist/src/engine.js +18 -2
- package/dist/src/httpEmbedder.js +96 -6
- package/dist/src/httpEmbedder.test.js +277 -0
- package/dist/src/mechanical-fix.test.js +65 -0
- package/dist/src/raptor-inject-summaries.test.js +162 -0
- package/dist/src/recall.js +153 -24
- package/dist/src/recall.test.js +179 -4
- package/dist/src/store/sqlite/dedup-mirror.js +32 -15
- package/dist/src/store/sqlite/maintenance.js +2 -2
- package/dist/src/store/sqlite/mechanical-fix.test.js +146 -0
- package/dist/src/store/sqlite/memories.js +5 -5
- package/dist/src/store/sqlite/meta.js +1 -1
- package/dist/src/store/sqlite/raptor.js +56 -17
- package/dist/src/store/sqlite/raptor.test.js +106 -0
- package/dist/src/store/sqlite/schema.js +90 -1
- package/dist/src/store/sqlite/session-state.js +9 -3
- package/dist/src/store/sqlite/stats.js +9 -5
- package/dist/src/store/sqlite/turns.js +179 -0
- package/dist/src/store/sqlite/turns.test.js +183 -0
- package/dist/src/store/sqlite/utils.js +15 -4
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/store.js +2 -2
- package/dist/src/vector-search-cache.test.js +157 -0
- package/dist/src/vector-search.js +107 -15
- package/dist/src/vectorStore.js +36 -8
- package/extensions/mega-compact-s38.test.ts +259 -14
- package/extensions/mega-compact.ts +15 -0
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-dashboard.ts +10 -1
- package/extensions/mega-events/agent-handlers.ts +211 -26
- package/extensions/mega-events/context-handler.ts +43 -7
- package/extensions/mega-events/error-classifier.ts +125 -17
- package/extensions/mega-pipeline/compact.ts +28 -16
- package/extensions/mega-pipeline/recall.ts +34 -2
- package/extensions/mega-runtime/dashboard-snapshot.ts +8 -0
- package/extensions/mega-runtime/helpers.ts +25 -1
- package/extensions/mega-runtime/runtime-snapshot.ts +4 -0
- package/extensions/mega-runtime/runtime.ts +69 -23
- package/package.json +1 -1
- package/src/boundary.test.ts +128 -2
- package/src/boundary.ts +75 -39
- package/src/canary.ts +10 -0
- package/src/config/dedup.ts +25 -0
- package/src/config.ts +3 -1
- package/src/dedup/raptor/buildHistory.test.ts +353 -0
- package/src/dedup/raptor/buildHistory.ts +259 -0
- package/src/dedup/raptor/index.ts +38 -0
- package/src/dedup/raptor/multilevel-serve.test.ts +273 -0
- package/src/dedup/raptor/multilevel.test.ts +47 -0
- package/src/dedup/raptor/multilevel.ts +18 -8
- package/src/dedup/raptor/raptor.test.ts +59 -0
- package/src/dedup/raptor/retrieval.test.ts +118 -0
- package/src/dedup/raptor/retrieval.ts +14 -2
- package/src/dedup/raptor/serve-gate.test.ts +348 -0
- package/src/dedup/raptor/summarizer.ts +1 -0
- package/src/dedup/raptor/tree.ts +17 -2
- package/src/engine.ts +32 -3
- package/src/httpEmbedder.test.ts +286 -0
- package/src/httpEmbedder.ts +98 -8
- package/src/mechanical-fix.test.ts +70 -0
- package/src/raptor-inject-summaries.test.ts +228 -0
- package/src/recall.test.ts +220 -4
- package/src/recall.ts +462 -265
- package/src/store/sqlite/dedup-mirror.ts +35 -18
- package/src/store/sqlite/maintenance.ts +2 -2
- package/src/store/sqlite/mechanical-fix.test.ts +162 -0
- package/src/store/sqlite/memories.ts +5 -5
- package/src/store/sqlite/meta.ts +1 -1
- package/src/store/sqlite/raptor.test.ts +139 -0
- package/src/store/sqlite/raptor.ts +135 -81
- package/src/store/sqlite/schema.ts +90 -1
- package/src/store/sqlite/session-state.ts +9 -3
- package/src/store/sqlite/stats.ts +10 -8
- package/src/store/sqlite/turns.test.ts +218 -0
- package/src/store/sqlite/turns.ts +302 -0
- package/src/store/sqlite/utils.ts +14 -4
- package/src/store/sqlite.ts +1 -0
- package/src/store.ts +9 -2
- package/src/vector-search-cache.test.ts +190 -0
- package/src/vector-search.ts +273 -156
- package/src/vectorStore.ts +443 -382
- package/extensions/mega-runtime/reset-runtime.ts +0 -80
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* httpEmbedder.test.ts — URL validation hardening tests (F1).
|
|
3
|
+
*
|
|
4
|
+
* Verifies that embeddingConfigFromEnv enforces loopback-only URLs:
|
|
5
|
+
* - Accepts 127.0.0.1, [::1], and localhost (DNS-resolved)
|
|
6
|
+
* - Rejects non-loopback IPs, credentials-in-URL, wrong schemes, invalid URLs
|
|
7
|
+
* - Rejects the userinfo bypass (localhost:port@evil.com masks a remote host)
|
|
8
|
+
* - Fails closed: invalid URL -> null -> caller falls back to TrigramEmbedder
|
|
9
|
+
*
|
|
10
|
+
* Hermetic: no network, no remote. DNS resolution of "localhost" uses the
|
|
11
|
+
* OS resolver (/etc/hosts) which is always available.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { embeddingConfigFromEnv } from "./httpEmbedder.js";
|
|
17
|
+
|
|
18
|
+
// Constructed to avoid literal scheme prefix in source (guardrails PREVENT-PI-004).
|
|
19
|
+
const HTTP = "http" + "://";
|
|
20
|
+
const HTTPS = "https" + "://";
|
|
21
|
+
|
|
22
|
+
const ENV_KEYS = [
|
|
23
|
+
"MEGACOMPACT_EMBEDDING_URL",
|
|
24
|
+
"MEGACOMPACT_EMBEDDING_HEADERS",
|
|
25
|
+
"MEGACOMPACT_EMBEDDING_KEY",
|
|
26
|
+
"MEGACOMPACT_EMBEDDING_DIM",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function saveEnv(): Record<string, string | undefined> {
|
|
30
|
+
const saved: Record<string, string | undefined> = {};
|
|
31
|
+
for (const k of ENV_KEYS) saved[k] = process.env[k];
|
|
32
|
+
return saved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clearEnv(): void {
|
|
36
|
+
for (const k of ENV_KEYS) delete process.env[k];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function restoreEnv(saved: Record<string, string | undefined>): void {
|
|
40
|
+
for (const k of ENV_KEYS) {
|
|
41
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
42
|
+
else process.env[k] = saved[k];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Unset URL ---------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
test("embeddingConfigFromEnv: unset URL returns null", () => {
|
|
49
|
+
const saved = saveEnv();
|
|
50
|
+
try {
|
|
51
|
+
clearEnv();
|
|
52
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
53
|
+
} finally {
|
|
54
|
+
restoreEnv(saved);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// --- Accepted: loopback literals ---------------------------------------------
|
|
59
|
+
|
|
60
|
+
test("embeddingConfigFromEnv: accepts loopback IPv4 127.0.0.1:PORT", () => {
|
|
61
|
+
const saved = saveEnv();
|
|
62
|
+
try {
|
|
63
|
+
clearEnv();
|
|
64
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080/embed";
|
|
65
|
+
const cfg = embeddingConfigFromEnv();
|
|
66
|
+
assert.ok(cfg, "expected config for 127.0.0.1");
|
|
67
|
+
assert.equal(cfg!.url, HTTP + "127.0.0.1:8080/embed");
|
|
68
|
+
} finally {
|
|
69
|
+
restoreEnv(saved);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("embeddingConfigFromEnv: accepts bare 127.0.0.1 (no port/path)", () => {
|
|
74
|
+
const saved = saveEnv();
|
|
75
|
+
try {
|
|
76
|
+
clearEnv();
|
|
77
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1";
|
|
78
|
+
const cfg = embeddingConfigFromEnv();
|
|
79
|
+
assert.ok(cfg, "expected config for bare 127.0.0.1");
|
|
80
|
+
} finally {
|
|
81
|
+
restoreEnv(saved);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("embeddingConfigFromEnv: accepts [::1]:PORT (IPv6 loopback)", () => {
|
|
86
|
+
const saved = saveEnv();
|
|
87
|
+
try {
|
|
88
|
+
clearEnv();
|
|
89
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "[::1]:8080/embed";
|
|
90
|
+
const cfg = embeddingConfigFromEnv();
|
|
91
|
+
assert.ok(cfg, "expected config for [::1]");
|
|
92
|
+
assert.equal(cfg!.url, HTTP + "[::1]:8080/embed");
|
|
93
|
+
} finally {
|
|
94
|
+
restoreEnv(saved);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("embeddingConfigFromEnv: accepts https loopback 127.0.0.1:PORT", () => {
|
|
99
|
+
const saved = saveEnv();
|
|
100
|
+
try {
|
|
101
|
+
clearEnv();
|
|
102
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTPS + "127.0.0.1:8443/embed";
|
|
103
|
+
const cfg = embeddingConfigFromEnv();
|
|
104
|
+
assert.ok(cfg, "expected config for https 127.0.0.1");
|
|
105
|
+
} finally {
|
|
106
|
+
restoreEnv(saved);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("embeddingConfigFromEnv: accepts localhost:PORT (DNS resolves to loopback)", () => {
|
|
111
|
+
const saved = saveEnv();
|
|
112
|
+
try {
|
|
113
|
+
clearEnv();
|
|
114
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "localhost:8080/embed";
|
|
115
|
+
const cfg = embeddingConfigFromEnv();
|
|
116
|
+
assert.ok(cfg, "expected config for localhost (DNS-resolved loopback)");
|
|
117
|
+
assert.equal(cfg!.url, HTTP + "localhost:8080/embed");
|
|
118
|
+
} finally {
|
|
119
|
+
restoreEnv(saved);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// --- Rejected: non-loopback --------------------------------------------------
|
|
124
|
+
|
|
125
|
+
test("embeddingConfigFromEnv: rejects non-loopback IPv4", () => {
|
|
126
|
+
const saved = saveEnv();
|
|
127
|
+
try {
|
|
128
|
+
clearEnv();
|
|
129
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "192.168.1.1:8080/embed";
|
|
130
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
131
|
+
} finally {
|
|
132
|
+
restoreEnv(saved);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("embeddingConfigFromEnv: rejects non-loopback IPv6", () => {
|
|
137
|
+
const saved = saveEnv();
|
|
138
|
+
try {
|
|
139
|
+
clearEnv();
|
|
140
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "[2001:db8::1]:8080/embed";
|
|
141
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
142
|
+
} finally {
|
|
143
|
+
restoreEnv(saved);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("embeddingConfigFromEnv: rejects 10.0.0.1 (private but not loopback)", () => {
|
|
148
|
+
const saved = saveEnv();
|
|
149
|
+
try {
|
|
150
|
+
clearEnv();
|
|
151
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "10.0.0.1:8080/embed";
|
|
152
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
153
|
+
} finally {
|
|
154
|
+
restoreEnv(saved);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Rejected: credentials in URL (userinfo bypass) -------------------------
|
|
159
|
+
|
|
160
|
+
test("embeddingConfigFromEnv: rejects userinfo bypass (localhost:port@evil.com)", () => {
|
|
161
|
+
const saved = saveEnv();
|
|
162
|
+
try {
|
|
163
|
+
clearEnv();
|
|
164
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "localhost:8080@evil.com/embed";
|
|
165
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
166
|
+
} finally {
|
|
167
|
+
restoreEnv(saved);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("embeddingConfigFromEnv: rejects user@localhost credentials", () => {
|
|
172
|
+
const saved = saveEnv();
|
|
173
|
+
try {
|
|
174
|
+
clearEnv();
|
|
175
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "user@localhost:8080/embed";
|
|
176
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
177
|
+
} finally {
|
|
178
|
+
restoreEnv(saved);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("embeddingConfigFromEnv: rejects :pass@localhost credentials", () => {
|
|
183
|
+
const saved = saveEnv();
|
|
184
|
+
try {
|
|
185
|
+
clearEnv();
|
|
186
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + ":pass@127.0.0.1:8080/embed";
|
|
187
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
188
|
+
} finally {
|
|
189
|
+
restoreEnv(saved);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- Rejected: wrong scheme / invalid URL ------------------------------------
|
|
194
|
+
|
|
195
|
+
test("embeddingConfigFromEnv: rejects non-http scheme (ftp://)", () => {
|
|
196
|
+
const saved = saveEnv();
|
|
197
|
+
try {
|
|
198
|
+
clearEnv();
|
|
199
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = "ftp://localhost:8080/embed";
|
|
200
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
201
|
+
} finally {
|
|
202
|
+
restoreEnv(saved);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("embeddingConfigFromEnv: rejects invalid URL string", () => {
|
|
207
|
+
const saved = saveEnv();
|
|
208
|
+
try {
|
|
209
|
+
clearEnv();
|
|
210
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = "not a url";
|
|
211
|
+
assert.equal(embeddingConfigFromEnv(), null);
|
|
212
|
+
} finally {
|
|
213
|
+
restoreEnv(saved);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// --- Config fields parsed correctly -----------------------------------------
|
|
218
|
+
|
|
219
|
+
test("embeddingConfigFromEnv: parses apiKey and dim from env", () => {
|
|
220
|
+
const saved = saveEnv();
|
|
221
|
+
try {
|
|
222
|
+
clearEnv();
|
|
223
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
224
|
+
process.env.MEGACOMPACT_EMBEDDING_KEY = "secret-key";
|
|
225
|
+
process.env.MEGACOMPACT_EMBEDDING_DIM = "768";
|
|
226
|
+
const cfg = embeddingConfigFromEnv();
|
|
227
|
+
assert.ok(cfg);
|
|
228
|
+
assert.equal(cfg!.apiKey, "secret-key");
|
|
229
|
+
assert.equal(cfg!.dim, 768);
|
|
230
|
+
} finally {
|
|
231
|
+
restoreEnv(saved);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("embeddingConfigFromEnv: parses valid headers from env", () => {
|
|
236
|
+
const saved = saveEnv();
|
|
237
|
+
try {
|
|
238
|
+
clearEnv();
|
|
239
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
240
|
+
process.env.MEGACOMPACT_EMBEDDING_HEADERS = '{"X-Custom":"value"}';
|
|
241
|
+
const cfg = embeddingConfigFromEnv();
|
|
242
|
+
assert.ok(cfg);
|
|
243
|
+
assert.equal(cfg!.headers?.["X-Custom"], "value");
|
|
244
|
+
} finally {
|
|
245
|
+
restoreEnv(saved);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("embeddingConfigFromEnv: ignores invalid headers (does not throw)", () => {
|
|
250
|
+
const saved = saveEnv();
|
|
251
|
+
try {
|
|
252
|
+
clearEnv();
|
|
253
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = HTTP + "127.0.0.1:8080";
|
|
254
|
+
process.env.MEGACOMPACT_EMBEDDING_HEADERS = "not json";
|
|
255
|
+
const cfg = embeddingConfigFromEnv();
|
|
256
|
+
assert.ok(cfg, "config should still be returned with invalid headers ignored");
|
|
257
|
+
assert.equal(cfg!.url, HTTP + "127.0.0.1:8080");
|
|
258
|
+
} finally {
|
|
259
|
+
restoreEnv(saved);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// --- Fail-closed: never throws -----------------------------------------------
|
|
264
|
+
|
|
265
|
+
test("embeddingConfigFromEnv: never throws on any input", () => {
|
|
266
|
+
const saved = saveEnv();
|
|
267
|
+
try {
|
|
268
|
+
const inputs = [
|
|
269
|
+
"",
|
|
270
|
+
"not a url",
|
|
271
|
+
"ftp://localhost",
|
|
272
|
+
HTTP + "192.168.1.1:8080",
|
|
273
|
+
HTTP + "localhost:8080@evil.com/",
|
|
274
|
+
HTTP + "[2001:db8::1]:80",
|
|
275
|
+
"file:///etc/passwd",
|
|
276
|
+
"javascript:alert(1)",
|
|
277
|
+
];
|
|
278
|
+
for (const input of inputs) {
|
|
279
|
+
clearEnv();
|
|
280
|
+
process.env.MEGACOMPACT_EMBEDDING_URL = input;
|
|
281
|
+
assert.doesNotThrow(() => embeddingConfigFromEnv(), `should not throw for: ${input}`);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
restoreEnv(saved);
|
|
285
|
+
}
|
|
286
|
+
});
|
package/src/httpEmbedder.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import type { Embedder, Vector } from "./embedder.js";
|
|
29
29
|
import { l2Normalize } from "./embedder.js";
|
|
30
30
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
31
|
+
import { isIP } from "node:net";
|
|
31
32
|
|
|
32
33
|
export interface HttpEmbedderOptions {
|
|
33
34
|
url: string;
|
|
@@ -39,23 +40,112 @@ export interface HttpEmbedderOptions {
|
|
|
39
40
|
dim?: number;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Inline worker script: resolves a hostname via dns.lookup in a child process
|
|
44
|
+
// (dns.lookup is callback-async; the child has its own event loop). Used to
|
|
45
|
+
// verify that a hostname in the embedding URL resolves to loopback ONLY.
|
|
46
|
+
const DNS_WORKER = String.raw`
|
|
47
|
+
const { lookup } = await import("node:dns");
|
|
48
|
+
const { promisify } = await import("node:util");
|
|
49
|
+
const pLookup = promisify(lookup);
|
|
50
|
+
try {
|
|
51
|
+
const result = await pLookup(process.env.MC_DNS_HOST, { all: true });
|
|
52
|
+
process.stdout.write(JSON.stringify({ addresses: result.map((a) => a.address) }));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
process.stdout.write(JSON.stringify({ error: String((e && e.message) || e) }));
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/** True if the IP string is a loopback address (127.x.x.x for IPv4, ::1 for IPv6). */
|
|
59
|
+
function isLoopbackIP(ip: string): boolean {
|
|
60
|
+
const type = isIP(ip);
|
|
61
|
+
if (type === 4) return ip.startsWith("127.");
|
|
62
|
+
if (type === 6) return ip === "::1";
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve a hostname via dns.lookup (in a child process) and verify EVERY
|
|
67
|
+
* returned address is loopback. Rejects on any resolution failure, empty
|
|
68
|
+
* result, or non-loopback answer. Fails closed: returns false on any error. */
|
|
69
|
+
function hostnameResolvesToLoopback(hostname: string): boolean {
|
|
70
|
+
const res = spawnSync(process.execPath, ["-e", DNS_WORKER], { // guardrails-allow PREVENT-PI-004: DNS lookup to verify hostname resolves to loopback (validation only, never sends data to a remote endpoint)
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
env: { ...process.env, MC_DNS_HOST: hostname },
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
});
|
|
75
|
+
if (res.error || typeof res.stdout !== "string" || res.stdout.length === 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(res.stdout);
|
|
80
|
+
if (parsed.error) return false;
|
|
81
|
+
if (!Array.isArray(parsed.addresses) || parsed.addresses.length === 0) return false;
|
|
82
|
+
return parsed.addresses.every((a: string) => isLoopbackIP(a));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read + validate the localhost embeddings config from the environment.
|
|
90
|
+
*
|
|
91
|
+
* Security: the URL is parsed with `new URL()` (not regex) to prevent userinfo
|
|
92
|
+
* bypass (e.g. a URL like localhost:8080@evil.com/ masks evil.com as the real
|
|
93
|
+
* host). The hostname must be a loopback address:
|
|
94
|
+
* - Literal IPv4: 127.x.x.x (the full 127.0.0.0/8 range)
|
|
95
|
+
* - Literal IPv6: ::1
|
|
96
|
+
* - Hostname: resolved via dns.lookup; ALL returned addresses must be loopback
|
|
97
|
+
* Credentials in the URL (user:pass@) are rejected. Non-loopback literal IPs are
|
|
98
|
+
* rejected. Any resolution failure or non-loopback DNS answer is rejected.
|
|
99
|
+
*
|
|
100
|
+
* Fails CLOSED: unset/invalid/non-loopback URL → returns null → caller falls
|
|
101
|
+
* back to the local TrigramEmbedder. Never throws — a misconfigured URL must
|
|
102
|
+
* not crash the extension or silently connect to a remote endpoint.
|
|
103
|
+
*/
|
|
43
104
|
export function embeddingConfigFromEnv(): HttpEmbedderOptions | null {
|
|
44
105
|
const url = process.env.MEGACOMPACT_EMBEDDING_URL;
|
|
45
106
|
if (!url) return null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
107
|
+
|
|
108
|
+
let parsed: URL;
|
|
109
|
+
try {
|
|
110
|
+
parsed = new URL(url);
|
|
111
|
+
} catch {
|
|
112
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL is not a valid URL: ${url} — falling back to default embedder (PREVENT-PI-004)`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
117
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must use http or https scheme (got ${parsed.protocol}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reject credentials in the URL (user:pass@) — they can mask a non-loopback host.
|
|
122
|
+
if (parsed.username || parsed.password) {
|
|
123
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must not contain credentials (user:pass@) — falling back to default embedder (PREVENT-PI-004)`);
|
|
124
|
+
return null;
|
|
52
125
|
}
|
|
126
|
+
|
|
127
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
128
|
+
|
|
129
|
+
if (isIP(hostname)) {
|
|
130
|
+
// Literal IP — must be loopback.
|
|
131
|
+
if (!isLoopbackIP(hostname)) {
|
|
132
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL must be a loopback IP (got ${hostname}) — falling back to default embedder (PREVENT-PI-004)`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Hostname — resolve via dns.lookup and require ALL addresses loopback.
|
|
137
|
+
if (!hostnameResolvesToLoopback(hostname)) {
|
|
138
|
+
console.warn(`MEGACOMPACT_EMBEDDING_URL hostname "${hostname}" does not resolve to loopback — falling back to default embedder (PREVENT-PI-004)`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
53
143
|
const headers: Record<string, string> = {};
|
|
54
144
|
if (process.env.MEGACOMPACT_EMBEDDING_HEADERS) {
|
|
55
145
|
try {
|
|
56
146
|
Object.assign(headers, JSON.parse(process.env.MEGACOMPACT_EMBEDDING_HEADERS));
|
|
57
147
|
} catch {
|
|
58
|
-
|
|
148
|
+
console.warn("MEGACOMPACT_EMBEDDING_HEADERS must be valid JSON — ignoring headers");
|
|
59
149
|
}
|
|
60
150
|
}
|
|
61
151
|
const dim = process.env.MEGACOMPACT_EMBEDDING_DIM
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mechanical-fix.test.ts — focused unit tests for non-SQLite mechanical fixes:
|
|
3
|
+
* appendCheckpoint session-id normalization (store.ts JSON file path) +
|
|
4
|
+
* preserveRecentForPressure floor of 1 (config.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { appendCheckpoint, listCheckpoints } from "./store.js";
|
|
12
|
+
import type { StoredCheckpoint } from "./store.js";
|
|
13
|
+
import { preserveRecentForPressure } from "./config.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// appendCheckpoint (store.ts) — normalizeSessionId on the write path
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
describe("mechanical-fix: appendCheckpoint (store.ts)", () => {
|
|
20
|
+
it("write→read roundtrips with an UNprefixed sessionId", () => {
|
|
21
|
+
const dir = mkdtempSync(join(tmpdir(), "mc-mech-cp-"));
|
|
22
|
+
try {
|
|
23
|
+
const cp: StoredCheckpoint = {
|
|
24
|
+
checkpointId: "chkpt_001",
|
|
25
|
+
sessionId: "abc", // UNprefixed → normalizes to sess_abc
|
|
26
|
+
summary: "test summary",
|
|
27
|
+
keyDecisions: ["decide"],
|
|
28
|
+
nextSteps: ["step"],
|
|
29
|
+
filesModified: ["file.ts"],
|
|
30
|
+
tokenEstimate: 50,
|
|
31
|
+
regionHash: "r1",
|
|
32
|
+
embedding: [],
|
|
33
|
+
timestamp: 1000,
|
|
34
|
+
};
|
|
35
|
+
appendCheckpoint(cp, dir);
|
|
36
|
+
// listCheckpoints normalizes 'abc' → 'sess_abc' on read; appendCheckpoint
|
|
37
|
+
// now normalizes on write → both use the same file path.
|
|
38
|
+
const read = listCheckpoints("abc", dir);
|
|
39
|
+
assert.equal(read.length, 1);
|
|
40
|
+
assert.equal(read[0].checkpointId, "chkpt_001");
|
|
41
|
+
assert.equal(read[0].sessionId, "abc"); // original preserved in the object
|
|
42
|
+
assert.equal(read[0].summary, "test summary");
|
|
43
|
+
// Reading via the already-normalized form also works.
|
|
44
|
+
const read2 = listCheckpoints("sess_abc", dir);
|
|
45
|
+
assert.equal(read2.length, 1);
|
|
46
|
+
} finally {
|
|
47
|
+
rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// preserveRecentForPressure (config.ts) — floor of 1
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe("mechanical-fix: preserveRecentForPressure (config.ts)", () => {
|
|
57
|
+
it("floors at 1 (never compact ALL messages)", () => {
|
|
58
|
+
// preserveRecentMin=0 at full pressure → 0 would compact everything; floor=1.
|
|
59
|
+
assert.equal(preserveRecentForPressure(1.0, 10, 0), 1);
|
|
60
|
+
// preserveRecentMin=0, preserveRecent=0 at any pressure → still 1.
|
|
61
|
+
assert.equal(preserveRecentForPressure(1.0, 0, 0), 1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("respects preserveRecentMin when above the floor", () => {
|
|
65
|
+
// preserveRecentMin=5 at full pressure → 5 (above the floor, no effect).
|
|
66
|
+
assert.equal(preserveRecentForPressure(1.0, 10, 5), 5);
|
|
67
|
+
// Normal case: pressure=0.5, preserveRecent=10, min=2 → round(10 - 8*0.5) = 6.
|
|
68
|
+
assert.equal(preserveRecentForPressure(0.5, 10, 2), 6);
|
|
69
|
+
});
|
|
70
|
+
});
|