@cnkang/qdrant-memory-launcher 0.0.0-stage → 0.1.0-rc.2

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/provision.mjs ADDED
@@ -0,0 +1,254 @@
1
+ /** Host-neutral provisioning. All data operations use the supported Runtime CLI. */
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { constants } from 'node:fs';
5
+ import { mkdir, lstat, readFile, writeFile, rename, rm, realpath, open } from 'node:fs/promises';
6
+ import { homedir } from 'node:os';
7
+ import { join, dirname, parse, resolve, relative, isAbsolute, basename } from 'node:path';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { BOOTSTRAPS, UV_VERSION, PYTHON_VERSION, download, extractExecutable, verifyArtifact } from './bootstrap.mjs';
10
+
11
+ export const runtime = JSON.parse(await readFile(new URL('./runtime.json', import.meta.url), 'utf8'));
12
+ export const hosts = new Set(['workbuddy', 'dsh']);
13
+ const requirements = (await readFile(new URL('./requirements.lock', import.meta.url), 'utf8')).replaceAll('\r\n', '\n');
14
+ export const dependencyDigest = createHash('sha256').update(requirements).update(JSON.stringify(runtime)).digest('hex');
15
+ const installedSnapshot = JSON.stringify({ revision: runtime.revision, dependencies: dependencyDigest });
16
+
17
+ export function runtimeRequirement(artifact, metadata = runtime, lock = requirements) {
18
+ lock = lock.replaceAll('\r\n', '\n');
19
+ if (metadata.status !== 'released-wheel') throw new Error('Unknown Runtime distribution');
20
+ const requirement = `hermes-plugin-qdrant-memory==${metadata.version}`;
21
+ const matches = [...lock.matchAll(/^hermes-plugin-qdrant-memory[^\n]*\n(?:[ \t]+--hash=sha256:[0-9a-f]+(?: \\)?\n)+/gm)];
22
+ if (matches.length !== 1 || !matches[0][0].startsWith(requirement + ' \\\n')
23
+ || !matches[0][0].includes(`--hash=sha256:${metadata.sha256}`)) throw new Error('Runtime dependency lock mismatch');
24
+ // A local wheel has one accepted artifact. Do not retain the registry sdist
25
+ // alternative when installing the downloaded and independently verified wheel.
26
+ return lock.replace(matches[0][0], `hermes-plugin-qdrant-memory @ ${pathToFileURL(artifact).href} \\\n --hash=sha256:${metadata.sha256}\n`);
27
+ }
28
+
29
+ export function locations(host, env = process.env, home = homedir(), platform = process.platform) {
30
+ if (!hosts.has(host)) throw new Error('Choose workbuddy or dsh');
31
+ const data = platform === 'win32' ? (env.LOCALAPPDATA || join(home, 'AppData', 'Local'))
32
+ : platform === 'darwin' ? join(home, 'Library', 'Application Support')
33
+ : (env.XDG_DATA_HOME || join(home, '.local', 'share'));
34
+ if (!isAbsolute(data)) throw new Error('Managed data root must be absolute');
35
+ const root = env.QDRANT_MEMORY_MANAGED_ROOT || join(data, 'qdrant-memory-managed');
36
+ if (!isAbsolute(root)) throw new Error('Managed root must be absolute');
37
+ return { root, program: join(root, 'programs', runtime.revision, dependencyDigest.slice(0, 12), `${platform}-${process.arch}`),
38
+ profile: join(root, 'profiles', host, 'local'), models: join(root, 'models', runtime.model_revision || runtime.revision) };
39
+ }
40
+
41
+ export async function modelCache(paths) {
42
+ // Keep the cache chosen by an existing profile across program upgrades.
43
+ // Runtime setup still verifies the complete profile ownership/configuration.
44
+ const config = join(paths.profile, 'qdrant-memory.json');
45
+ await privateDirectory(paths.profile);
46
+ try {
47
+ const file = await open(config, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
48
+ let value;
49
+ try {
50
+ const info = await file.stat();
51
+ const entry = await lstat(config);
52
+ if (!info.isFile() || info.size > 65536 || entry.isSymbolicLink()
53
+ || info.ino !== entry.ino || info.dev !== entry.dev) throw new Error('Unsafe profile configuration');
54
+ value = JSON.parse(await file.readFile('utf8'));
55
+ } finally { await file.close(); }
56
+ const cache = value.embedding?.model_cache_dir;
57
+ if (typeof cache !== 'string' || !isAbsolute(cache)) throw new Error('Invalid managed model cache');
58
+ const inside = relative(join(paths.root, 'models'), cache);
59
+ if (!inside || inside.startsWith('..') || isAbsolute(inside)) throw new Error('Model cache escaped managed root');
60
+ await privateDirectory(cache);
61
+ return cache;
62
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
63
+ return paths.models;
64
+ }
65
+
66
+ export async function privateDirectory(path) {
67
+ const absolute = resolve(path);
68
+ let current = parse(absolute).root;
69
+ for (const part of absolute.slice(current.length).split(/[\\/]/).filter(Boolean)) {
70
+ current = join(current, part);
71
+ await mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
72
+ const info = await lstat(current);
73
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error('Unsafe managed directory');
74
+ }
75
+ return absolute;
76
+ }
77
+
78
+ export function installEnvironment(program, env = process.env) {
79
+ const clean = { PATH: '', UV_NO_CONFIG: '1', UV_CACHE_DIR: join(program, 'cache'),
80
+ UV_PYTHON_INSTALL_DIR: join(program, 'python'), UV_PYTHON_PREFERENCE: 'only-managed',
81
+ UV_PYTHON_DOWNLOADS: 'automatic', UV_NO_PROGRESS: '1', PYTHONNOUSERSITE: '1' };
82
+ for (const name of ['SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP', 'TMPDIR']) if (env[name]) clean[name] = env[name];
83
+ return clean;
84
+ }
85
+
86
+ export function run(executable, args, { env, signal, timeout = 600000 } = {}) {
87
+ return new Promise((accept, reject) => {
88
+ const child = spawn(executable, args, { env, windowsHide: true, detached: process.platform !== 'win32',
89
+ stdio: ['ignore', 'pipe', 'ignore'] });
90
+ let output = '', settled = false;
91
+ const stop = () => {
92
+ if (!child.pid) return;
93
+ if (process.platform === 'win32') {
94
+ // Windows has no POSIX process groups. Use the OS tree-termination utility.
95
+ const systemRoot = process.env.SYSTEMROOT || 'C:\\Windows';
96
+ spawn(join(systemRoot, 'System32', 'taskkill.exe'), ['/pid', String(child.pid), '/T', '/F'],
97
+ { windowsHide: true, stdio: 'ignore' }).unref();
98
+ } else { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already closed */ } }
99
+ };
100
+ const timer = setTimeout(stop, timeout);
101
+ signal?.addEventListener('abort', stop, { once: true });
102
+ if (signal?.aborted) stop();
103
+ child.stdout.on('data', bytes => { output += bytes; if (output.length > 1024 * 1024) stop(); });
104
+ const finish = (ok) => {
105
+ if (settled) return;
106
+ settled = true;
107
+ clearTimeout(timer);
108
+ signal?.removeEventListener('abort', stop);
109
+ if (ok && !signal?.aborted) accept(output.trim());
110
+ else reject(new Error('Managed subprocess failed; retry preparation and check network/disk access'));
111
+ };
112
+ child.on('error', () => finish(false));
113
+ child.on('close', code => finish(code === 0));
114
+ });
115
+ }
116
+
117
+ async function regularFile(path) {
118
+ const info = await lstat(path);
119
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('Unsafe managed file');
120
+ return path;
121
+ }
122
+
123
+ // Windows can briefly hold a freshly written file open: a real-time scan of a
124
+ // new executable, or a terminating child that still maps it after a client
125
+ // stopped its probe. Retry only transient lock errors, within bounds.
126
+ export async function publishRename(from, to, signal) {
127
+ const delays = [50, 100, 200, 400, 800, 1500, 1500, 1500, 1500, 1500];
128
+ for (let attempt = 0; ; attempt++) {
129
+ try {
130
+ await rename(from, to);
131
+ return;
132
+ } catch (error) {
133
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || attempt >= delays.length || signal?.aborted) throw error;
134
+ await new Promise(resolve => setTimeout(resolve, delays[attempt]));
135
+ }
136
+ }
137
+ }
138
+
139
+ // Caller must hold the program's provisioning lock. Only a prefix of the exact
140
+ // expected snapshot can be left by an interrupted legacy write; other changes
141
+ // remain errors. Publish on the same filesystem only after writing and flushing.
142
+ export async function publishSnapshot(path, content) {
143
+ let existing;
144
+ try { await regularFile(path); existing = await readFile(path, 'utf8'); }
145
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
146
+ if (existing !== undefined && !content.startsWith(existing)) throw new Error(`Managed snapshot changed: ${basename(path)}`);
147
+ const temporary = path + '.pending';
148
+ try { await regularFile(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; }
149
+ await rm(temporary, { force: true });
150
+ if (existing === content) return;
151
+ try {
152
+ const file = await open(temporary, 'wx', 0o600);
153
+ try { await file.writeFile(content); await file.sync(); }
154
+ finally { await file.close(); }
155
+ await publishRename(temporary, path);
156
+ } finally { await rm(temporary, { force: true }); }
157
+ }
158
+
159
+ export async function installed(paths) {
160
+ // A marker is committed only after package installation completes. Missing or
161
+ // partial installs are never executable; retries use the same provisioning lock.
162
+ await privateDirectory(paths.program);
163
+ const marker = join(paths.program, 'installed.json');
164
+ await regularFile(marker);
165
+ const value = JSON.parse(await readFile(marker, 'utf8'));
166
+ if (value.revision !== runtime.revision || value.dependencies !== dependencyDigest) throw new Error('Runtime revision or dependency mismatch');
167
+ const executable = join(paths.program, 'env', process.platform === 'win32' ? 'Scripts' : 'bin',
168
+ process.platform === 'win32' ? 'qdrant-memory.exe' : 'qdrant-memory');
169
+ return regularFile(executable);
170
+ }
171
+
172
+ export async function prepare(paths, host, signal, onStage = () => {}) {
173
+ if (!hosts.has(host)) throw new Error('Unknown host');
174
+ await privateDirectory(paths.program);
175
+ const lock = join(paths.program, '.provision.lock');
176
+ // Never steal an old lock: a concurrent installer may still own Python children.
177
+ await mkdir(lock, { mode: 0o700 }).catch(() => { throw new Error('Preparation is already running or interrupted; inspect the provisioning lock'); });
178
+ let stage = 'inspect-installation';
179
+ const enter = value => { stage = value; onStage(value); };
180
+ try {
181
+ let executable;
182
+ try { executable = await installed(paths); } catch (error) {
183
+ if (error instanceof SyntaxError) {
184
+ const marker = join(paths.program, 'installed.json');
185
+ // A truncated legacy marker is not installation success. Retry package
186
+ // validation/installation before committing a new marker; never run it.
187
+ const partial = await readFile(marker, 'utf8');
188
+ if (!installedSnapshot.startsWith(partial)) throw error;
189
+ await rm(marker);
190
+ } else if (error.code !== 'ENOENT') throw error;
191
+ }
192
+ const env = installEnvironment(paths.program);
193
+ if (!executable) {
194
+ const spec = BOOTSTRAPS[`${process.platform}-${process.arch}`];
195
+ if (!spec) throw new Error('Unsupported managed platform');
196
+ const uv = join(paths.program, process.platform === 'win32' ? 'uv.exe' : 'uv');
197
+ enter('download-bootstrap');
198
+ // Publish only a fully verified regular executable, never an archive path.
199
+ const bytes = extractExecutable(await download(spec, signal), `uv-${UV_VERSION}.data/scripts/${process.platform === 'win32' ? 'uv.exe' : 'uv'}`);
200
+ const temporary = uv + '.pending';
201
+ // A stopped probe can leave a stale temporary file; the held lock makes removal safe.
202
+ await rm(temporary, { force: true });
203
+ await writeFile(temporary, bytes, { flag: 'wx', mode: 0o700 });
204
+ if (await lstat(uv).catch(error => { if (error.code !== 'ENOENT') throw error; })) await regularFile(uv);
205
+ await publishRename(temporary, uv, signal);
206
+ enter('install-python');
207
+ await run(uv, ['python', 'install', PYTHON_VERSION, '--no-bin'], { env, signal });
208
+ const python = await run(uv, ['python', 'find', '--system', PYTHON_VERSION], { env, signal });
209
+ const inside = relative(await realpath(paths.program), await realpath(python));
210
+ if (!inside || inside.startsWith('..') || isAbsolute(inside)) throw new Error('Python escaped managed root');
211
+ // A client may stop a probe after discovery, interrupting preparation.
212
+ // Resume only this revision-owned environment; never clear its contents.
213
+ await run(uv, ['venv', '--allow-existing', '--python', python, join(paths.program, 'env')], { env, signal });
214
+ enter('download-runtime');
215
+ const archive = join(paths.program, `hermes_plugin_qdrant_memory-${runtime.version}-py3-none-any.whl`);
216
+ // Clear an interrupted temporary download even if the verified artifact
217
+ // survived. The provisioning lock owns only this program transaction.
218
+ await rm(archive + '.pending', { force: true });
219
+ try { await regularFile(archive); verifyArtifact(await readFile(archive), runtime); }
220
+ catch (error) {
221
+ if (error.code !== 'ENOENT') throw error;
222
+ // Publish the archive only after the full download is on disk, so an
223
+ // interrupted write can never leave a partial archive that blocks retries.
224
+ const temporaryArchive = archive + '.pending';
225
+ await rm(temporaryArchive, { force: true });
226
+ await writeFile(temporaryArchive, await download(runtime, signal), { mode: 0o600 });
227
+ await publishRename(temporaryArchive, archive, signal);
228
+ }
229
+ const environmentPython = join(paths.program, 'env', process.platform === 'win32' ? 'Scripts/python.exe' : 'bin/python');
230
+ // Install only published wheels. No source builds or build environment.
231
+ const resolvedRequirements = runtimeRequirement(archive);
232
+ const lockfile = join(paths.program, 'requirements.lock');
233
+ await publishSnapshot(lockfile, resolvedRequirements);
234
+ enter('install-runtime');
235
+ await run(uv, ['pip', 'install', '--python', environmentPython, '--no-deps', '--no-build-isolation', '--require-hashes',
236
+ '--only-binary=:all:',
237
+ '-r', lockfile], { env, signal });
238
+ await run(uv, ['pip', 'check', '--python', environmentPython], { env, signal });
239
+ await rm(archive);
240
+ await publishSnapshot(join(paths.program, 'installed.json'), installedSnapshot);
241
+ executable = await installed(paths);
242
+ }
243
+ await privateDirectory(dirname(paths.profile));
244
+ const models = await modelCache(paths);
245
+ await privateDirectory(models);
246
+ enter('setup-profile');
247
+ await run(executable, ['setup', '--profile', paths.profile, '--embedding-provider', 'fastembed',
248
+ '--model-cache-dir', models, '--agent-id', host], { env, signal });
249
+ return executable;
250
+ } catch (error) {
251
+ error.stage = stage;
252
+ throw error;
253
+ } finally { await rm(lock, { recursive: true }); }
254
+ }
@@ -0,0 +1,2 @@
1
+ # Optional CPU dependencies are isolated from native Hermes installations.
2
+ hermes-plugin-qdrant-memory[local,mcp]==0.3.0