superlocalmemory 3.4.19 → 3.4.22
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/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SuperLocalMemory v3.4.21 — slm-hook binary fetcher.
|
|
4
|
+
*
|
|
5
|
+
* LLD reference: .backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md §6.2
|
|
6
|
+
*
|
|
7
|
+
* Fails open: any network / SHA / unpack failure logs a warning and
|
|
8
|
+
* exits 0. The dispatcher (bin/slm) falls back to the Python path so
|
|
9
|
+
* the hook still works — just slower on Windows.
|
|
10
|
+
*
|
|
11
|
+
* Stdlib-only (node:https, node:crypto, node:fs, node:path, node:os).
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const fsp = require('node:fs/promises');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const crypto = require('node:crypto');
|
|
20
|
+
const https = require('node:https');
|
|
21
|
+
|
|
22
|
+
const SLM_HOME = path.join(os.homedir(), '.superlocalmemory');
|
|
23
|
+
const BIN_DIR = path.join(SLM_HOME, 'bin');
|
|
24
|
+
const MANIFEST_BASE_URL = process.env.SLM_MANIFEST_BASE_URL ||
|
|
25
|
+
'https://github.com/qualixar/superlocalmemory/releases/download';
|
|
26
|
+
|
|
27
|
+
// Map Node os.platform() / os.arch() -> canonical manifest tuple.
|
|
28
|
+
const PLATFORM_MAP = {
|
|
29
|
+
'darwin': 'macos',
|
|
30
|
+
'linux': 'linux',
|
|
31
|
+
'win32': 'windows',
|
|
32
|
+
};
|
|
33
|
+
const ARCH_MAP = {
|
|
34
|
+
'arm64': 'arm64',
|
|
35
|
+
'x64': 'x86_64',
|
|
36
|
+
'x86_64': 'x86_64',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function canonicalPlatformArch(platform, arch) {
|
|
40
|
+
const p = PLATFORM_MAP[platform] || null;
|
|
41
|
+
const a = ARCH_MAP[arch] || null;
|
|
42
|
+
return (p && a) ? { platform: p, arch: a } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function logInfo(msg) { console.log(`[slm-hook] ${msg}`); }
|
|
46
|
+
function logWarn(msg) { console.warn(`[slm-hook] ${msg}`); }
|
|
47
|
+
|
|
48
|
+
// S8-SEC-03: redirect host allow-list. Previously any 3xx ``Location``
|
|
49
|
+
// header would be followed blindly, so a compromised upstream (DNS
|
|
50
|
+
// hijack, typosquat in the package registry) could redirect the postinstall
|
|
51
|
+
// download to an attacker-controlled host and substitute the hook binary.
|
|
52
|
+
// We only follow redirects back to GitHub's own release CDN.
|
|
53
|
+
const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
54
|
+
'github.com',
|
|
55
|
+
'api.github.com',
|
|
56
|
+
'codeload.github.com',
|
|
57
|
+
'objects.githubusercontent.com',
|
|
58
|
+
'raw.githubusercontent.com',
|
|
59
|
+
'release-assets.githubusercontent.com',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
function redirectIsAllowed(rawLocation) {
|
|
63
|
+
if (typeof rawLocation !== 'string' || rawLocation.length === 0) return false;
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(rawLocation);
|
|
66
|
+
if (parsed.protocol !== 'https:') return false;
|
|
67
|
+
return ALLOWED_REDIRECT_HOSTS.has(parsed.hostname);
|
|
68
|
+
} catch (_err) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fetchJson(url) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const req = https.get(url, (res) => {
|
|
76
|
+
if (res.statusCode && res.statusCode >= 300 &&
|
|
77
|
+
res.statusCode < 400 && res.headers.location) {
|
|
78
|
+
if (!redirectIsAllowed(res.headers.location)) {
|
|
79
|
+
reject(new Error('redirect host not allow-listed'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Single redirect hop — GitHub releases use redirects.
|
|
83
|
+
fetchJson(res.headers.location).then(resolve, reject);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (res.statusCode !== 200) {
|
|
87
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const chunks = [];
|
|
91
|
+
res.on('data', (c) => chunks.push(c));
|
|
92
|
+
res.on('end', () => {
|
|
93
|
+
try {
|
|
94
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
reject(err);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
req.on('error', reject);
|
|
101
|
+
req.setTimeout(15000, () => {
|
|
102
|
+
req.destroy(new Error('timeout fetching manifest'));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function downloadFile(url, destPath) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const out = fs.createWriteStream(destPath);
|
|
110
|
+
const req = https.get(url, (res) => {
|
|
111
|
+
if (res.statusCode && res.statusCode >= 300 &&
|
|
112
|
+
res.statusCode < 400 && res.headers.location) {
|
|
113
|
+
if (!redirectIsAllowed(res.headers.location)) {
|
|
114
|
+
out.close();
|
|
115
|
+
fs.unlink(destPath, () => {});
|
|
116
|
+
reject(new Error('redirect host not allow-listed'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
out.close();
|
|
120
|
+
fs.unlink(destPath, () => {
|
|
121
|
+
downloadFile(res.headers.location, destPath)
|
|
122
|
+
.then(resolve, reject);
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (res.statusCode !== 200) {
|
|
127
|
+
out.close();
|
|
128
|
+
fs.unlink(destPath, () => {});
|
|
129
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
res.pipe(out);
|
|
133
|
+
out.on('finish', () => out.close(resolve));
|
|
134
|
+
});
|
|
135
|
+
req.on('error', (err) => {
|
|
136
|
+
out.close();
|
|
137
|
+
fs.unlink(destPath, () => {});
|
|
138
|
+
reject(err);
|
|
139
|
+
});
|
|
140
|
+
req.setTimeout(60000, () => {
|
|
141
|
+
req.destroy(new Error('timeout downloading asset'));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function sha256File(filePath) {
|
|
147
|
+
const h = crypto.createHash('sha256');
|
|
148
|
+
const stream = fs.createReadStream(filePath);
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
stream.on('data', (c) => h.update(c));
|
|
151
|
+
stream.on('end', () => resolve(h.digest('hex')));
|
|
152
|
+
stream.on('error', reject);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pickAsset(manifest, platform, arch) {
|
|
157
|
+
if (!manifest || !Array.isArray(manifest.assets)) return null;
|
|
158
|
+
// Prefer setup.exe on Windows (Inno Setup) when present, fall back
|
|
159
|
+
// to plain archive.
|
|
160
|
+
const matches = manifest.assets.filter(a =>
|
|
161
|
+
a.platform === platform && a.arch === arch);
|
|
162
|
+
if (matches.length === 0) return null;
|
|
163
|
+
// Deterministic ordering: setup.exe > .zip > .tar.gz
|
|
164
|
+
matches.sort((a, b) => {
|
|
165
|
+
const rank = (n) => n.endsWith('setup.exe') ? 0 :
|
|
166
|
+
n.endsWith('.zip') ? 1 : 2;
|
|
167
|
+
return rank(a.name) - rank(b.name);
|
|
168
|
+
});
|
|
169
|
+
return matches[0];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function main() {
|
|
173
|
+
try {
|
|
174
|
+
const pkgJson = require(path.join(__dirname, '..', 'package.json'));
|
|
175
|
+
const version = pkgJson.version;
|
|
176
|
+
const url = `${MANIFEST_BASE_URL}/v${version}/manifest.json`;
|
|
177
|
+
|
|
178
|
+
const pa = canonicalPlatformArch(os.platform(), os.arch());
|
|
179
|
+
if (!pa) {
|
|
180
|
+
logWarn(`unsupported platform ${os.platform()}/${os.arch()}; ` +
|
|
181
|
+
'Python fallback');
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let manifest;
|
|
186
|
+
try {
|
|
187
|
+
manifest = await fetchJson(url);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
logWarn(`manifest fetch failed: ${err.message}; Python fallback`);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const asset = pickAsset(manifest, pa.platform, pa.arch);
|
|
194
|
+
if (!asset) {
|
|
195
|
+
logWarn(`no prebuilt binary for ${pa.platform}/${pa.arch}; ` +
|
|
196
|
+
'Python fallback');
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// S8-SEC-01 fix: the manifest is remote-attacker-controllable (even
|
|
201
|
+
// when minisigned, the signer is a release process we don't fully
|
|
202
|
+
// trust at the per-asset level). Reject any ``asset.name`` that
|
|
203
|
+
// contains a path separator, traversal segments, or is empty —
|
|
204
|
+
// and re-derive the safe filename from basename before joining.
|
|
205
|
+
// Without this, a poisoned manifest can write arbitrary paths
|
|
206
|
+
// (e.g., "../../.ssh/authorized_keys") even though SHA-256 validates
|
|
207
|
+
// the bytes, because SHA only binds the content to the claimed name.
|
|
208
|
+
const rawName = typeof asset.name === 'string' ? asset.name : '';
|
|
209
|
+
if (!rawName
|
|
210
|
+
|| rawName.includes('/')
|
|
211
|
+
|| rawName.includes('\\')
|
|
212
|
+
|| rawName.includes('\0')
|
|
213
|
+
|| rawName === '.'
|
|
214
|
+
|| rawName === '..'
|
|
215
|
+
|| rawName.startsWith('.')) {
|
|
216
|
+
logWarn(`rejected asset name (path-traversal guard): ` +
|
|
217
|
+
`${JSON.stringify(rawName)}; Python fallback`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
const safeName = path.basename(rawName);
|
|
221
|
+
if (safeName !== rawName) {
|
|
222
|
+
logWarn(`asset name changed under basename sanitisation: ` +
|
|
223
|
+
`${JSON.stringify(rawName)} -> ${JSON.stringify(safeName)}; ` +
|
|
224
|
+
'Python fallback');
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await fsp.mkdir(BIN_DIR, { recursive: true });
|
|
229
|
+
const tmpName = `${safeName}.part`;
|
|
230
|
+
const tmpPath = path.join(BIN_DIR, tmpName);
|
|
231
|
+
try {
|
|
232
|
+
await downloadFile(asset.url, tmpPath);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logWarn(`download failed: ${err.message}; Python fallback`);
|
|
235
|
+
await fsp.unlink(tmpPath).catch(() => {});
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const actual = await sha256File(tmpPath);
|
|
240
|
+
if (actual !== asset.sha256) {
|
|
241
|
+
await fsp.unlink(tmpPath).catch(() => {});
|
|
242
|
+
logWarn(`SHA-256 mismatch for ${asset.name}: expected ` +
|
|
243
|
+
`${asset.sha256}, got ${actual}; Python fallback`);
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const finalPath = path.join(BIN_DIR, safeName);
|
|
248
|
+
// Belt-and-suspenders: after resolving the final path, assert it
|
|
249
|
+
// sits under BIN_DIR. ``path.resolve`` normalises any '..' that
|
|
250
|
+
// could have slipped through; if the result escapes BIN_DIR
|
|
251
|
+
// (which it shouldn't after basename sanitisation, but it's
|
|
252
|
+
// cheap insurance), refuse to install.
|
|
253
|
+
const resolvedFinal = path.resolve(finalPath);
|
|
254
|
+
const resolvedBin = path.resolve(BIN_DIR);
|
|
255
|
+
if (!resolvedFinal.startsWith(resolvedBin + path.sep)
|
|
256
|
+
&& resolvedFinal !== resolvedBin) {
|
|
257
|
+
await fsp.unlink(tmpPath).catch(() => {});
|
|
258
|
+
logWarn(`path escape detected: ${resolvedFinal} not under ` +
|
|
259
|
+
`${resolvedBin}; Python fallback`);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
await fsp.rename(tmpPath, finalPath);
|
|
263
|
+
logInfo(`✓ slm-hook asset fetched: ${asset.name} ` +
|
|
264
|
+
`(${asset.signing})`);
|
|
265
|
+
logInfo(` path: ${finalPath}`);
|
|
266
|
+
logInfo(' Note: unpack step is platform-specific; ' +
|
|
267
|
+
'the installer runs it.');
|
|
268
|
+
return 0;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
logWarn(`unexpected error: ${err.message}; Python fallback`);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Exports for testing; direct execution runs main().
|
|
276
|
+
module.exports = {
|
|
277
|
+
canonicalPlatformArch,
|
|
278
|
+
pickAsset,
|
|
279
|
+
sha256File,
|
|
280
|
+
MANIFEST_BASE_URL,
|
|
281
|
+
main,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (require.main === module) {
|
|
285
|
+
main().then((code) => process.exit(code || 0),
|
|
286
|
+
() => process.exit(0));
|
|
287
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4.22 — LLD-06 §6.1
|
|
4
|
+
|
|
5
|
+
"""Generate the release ``manifest.json`` for ``slm-hook`` binaries.
|
|
6
|
+
|
|
7
|
+
LLD reference: ``.backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md``
|
|
8
|
+
Section 6.1 (Release manifest).
|
|
9
|
+
|
|
10
|
+
Shape (per LLD-06 §6.1):
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
"version": "3.4.22",
|
|
14
|
+
"released_at": "2026-04-17T00:00:00Z",
|
|
15
|
+
"assets": [
|
|
16
|
+
{
|
|
17
|
+
"name": "slm-hook-macos-arm64.tar.gz",
|
|
18
|
+
"url": "https://.../manifest.json",
|
|
19
|
+
"size_bytes": 5312000,
|
|
20
|
+
"sha256": "a7c3...",
|
|
21
|
+
"signing": "apple-notarized"
|
|
22
|
+
}, ...
|
|
23
|
+
],
|
|
24
|
+
"manifest_sha256_self": "...",
|
|
25
|
+
"manifest_signature": "minisign: placeholder"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Minisign signing is OUT of scope at Stage 6 — this module emits a
|
|
29
|
+
placeholder and a ``manifest.minisig`` stub. Real signing is a CI job
|
|
30
|
+
that layers on top (needs the private key in GitHub Secrets).
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import hashlib
|
|
35
|
+
import json
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from datetime import datetime, timezone
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
# Signing envelope strings — one per expected platform. Must match the
|
|
41
|
+
# values the dispatcher + postinstall check. Kept as constants so tests
|
|
42
|
+
# can lock them.
|
|
43
|
+
SIGNING_APPLE_NOTARIZED = "apple-notarized"
|
|
44
|
+
SIGNING_AUTHENTICODE = "authenticode"
|
|
45
|
+
SIGNING_UNSIGNED = "unsigned"
|
|
46
|
+
|
|
47
|
+
# Default base URL used when the caller doesn't supply one. Kept in a
|
|
48
|
+
# constant so tests can assert the release shape without a network call.
|
|
49
|
+
DEFAULT_BASE_URL = (
|
|
50
|
+
"https://github.com/qualixar/superlocalmemory/releases/download"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Allowed platform/arch combos we ship binaries for. Any asset whose
|
|
54
|
+
# filename does not encode one of these is rejected.
|
|
55
|
+
_VALID_PLATFORMS: frozenset[tuple[str, str]] = frozenset({
|
|
56
|
+
("macos", "arm64"),
|
|
57
|
+
("macos", "x86_64"),
|
|
58
|
+
("linux", "x86_64"),
|
|
59
|
+
("linux", "arm64"),
|
|
60
|
+
("windows", "x86_64"),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class AssetSpec:
|
|
66
|
+
"""Input spec for a single release artefact on disk."""
|
|
67
|
+
|
|
68
|
+
path: Path
|
|
69
|
+
platform: str
|
|
70
|
+
arch: str
|
|
71
|
+
signing: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class Manifest:
|
|
76
|
+
"""In-memory representation of the manifest to be serialized."""
|
|
77
|
+
|
|
78
|
+
version: str
|
|
79
|
+
released_at: str
|
|
80
|
+
assets: list[dict] = field(default_factory=list)
|
|
81
|
+
manifest_sha256_self: str = ""
|
|
82
|
+
manifest_signature: str = "minisign: placeholder"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Helpers
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def _sha256_file(path: Path) -> str:
|
|
90
|
+
h = hashlib.sha256()
|
|
91
|
+
with path.open("rb") as fh:
|
|
92
|
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
93
|
+
h.update(chunk)
|
|
94
|
+
return h.hexdigest()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _utcnow_iso() -> str:
|
|
98
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _validate_platform(platform: str, arch: str) -> None:
|
|
102
|
+
if (platform, arch) not in _VALID_PLATFORMS:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"unsupported platform/arch: {platform}/{arch}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_signing(signing: str) -> None:
|
|
109
|
+
allowed = {SIGNING_APPLE_NOTARIZED, SIGNING_AUTHENTICODE,
|
|
110
|
+
SIGNING_UNSIGNED}
|
|
111
|
+
if signing not in allowed:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"invalid signing value: {signing!r} (allowed: {allowed})"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _asset_url(base_url: str, version: str, filename: str) -> str:
|
|
118
|
+
base = base_url.rstrip("/")
|
|
119
|
+
return f"{base}/v{version}/{filename}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Builders
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def build_asset_entry(
|
|
127
|
+
spec: AssetSpec,
|
|
128
|
+
*,
|
|
129
|
+
version: str,
|
|
130
|
+
base_url: str,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Compute a single manifest asset entry."""
|
|
133
|
+
if not spec.path.is_file():
|
|
134
|
+
raise FileNotFoundError(f"asset missing: {spec.path}")
|
|
135
|
+
_validate_platform(spec.platform, spec.arch)
|
|
136
|
+
_validate_signing(spec.signing)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"name": spec.path.name,
|
|
140
|
+
"url": _asset_url(base_url, version, spec.path.name),
|
|
141
|
+
"size_bytes": spec.path.stat().st_size,
|
|
142
|
+
"sha256": _sha256_file(spec.path),
|
|
143
|
+
"signing": spec.signing,
|
|
144
|
+
"platform": spec.platform,
|
|
145
|
+
"arch": spec.arch,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_manifest(
|
|
150
|
+
version: str,
|
|
151
|
+
assets: list[AssetSpec],
|
|
152
|
+
*,
|
|
153
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
154
|
+
released_at: str | None = None,
|
|
155
|
+
) -> Manifest:
|
|
156
|
+
"""Assemble the full Manifest object (including self SHA)."""
|
|
157
|
+
if not version:
|
|
158
|
+
raise ValueError("version is required")
|
|
159
|
+
if not assets:
|
|
160
|
+
raise ValueError("at least one asset is required")
|
|
161
|
+
|
|
162
|
+
entries: list[dict] = [
|
|
163
|
+
build_asset_entry(spec, version=version, base_url=base_url)
|
|
164
|
+
for spec in assets
|
|
165
|
+
]
|
|
166
|
+
payload = {
|
|
167
|
+
"version": version,
|
|
168
|
+
"released_at": released_at or _utcnow_iso(),
|
|
169
|
+
"assets": entries,
|
|
170
|
+
}
|
|
171
|
+
# Compute the manifest-self SHA over the canonical JSON WITHOUT the
|
|
172
|
+
# self-SHA field (otherwise it would be recursive).
|
|
173
|
+
serialized = json.dumps(payload, sort_keys=True,
|
|
174
|
+
separators=(",", ":")).encode("utf-8")
|
|
175
|
+
self_sha = hashlib.sha256(serialized).hexdigest()
|
|
176
|
+
|
|
177
|
+
return Manifest(
|
|
178
|
+
version=version,
|
|
179
|
+
released_at=payload["released_at"],
|
|
180
|
+
assets=entries,
|
|
181
|
+
manifest_sha256_self=self_sha,
|
|
182
|
+
manifest_signature="minisign: placeholder",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def serialize(manifest: Manifest) -> str:
|
|
187
|
+
"""Deterministic JSON serialization for on-disk storage."""
|
|
188
|
+
doc = {
|
|
189
|
+
"version": manifest.version,
|
|
190
|
+
"released_at": manifest.released_at,
|
|
191
|
+
"assets": manifest.assets,
|
|
192
|
+
"manifest_sha256_self": manifest.manifest_sha256_self,
|
|
193
|
+
"manifest_signature": manifest.manifest_signature,
|
|
194
|
+
}
|
|
195
|
+
return json.dumps(doc, indent=2, sort_keys=False) + "\n"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def write_manifest(
|
|
199
|
+
manifest: Manifest,
|
|
200
|
+
dest_dir: Path,
|
|
201
|
+
) -> tuple[Path, Path]:
|
|
202
|
+
"""Write ``manifest.json`` + a stub ``manifest.minisig`` to ``dest_dir``.
|
|
203
|
+
|
|
204
|
+
Returns the two paths (for caller convenience).
|
|
205
|
+
"""
|
|
206
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
manifest_path = dest_dir / "manifest.json"
|
|
208
|
+
sig_path = dest_dir / "manifest.minisig"
|
|
209
|
+
|
|
210
|
+
manifest_path.write_text(serialize(manifest), encoding="utf-8")
|
|
211
|
+
# Placeholder — real minisig produced by a separate CI job.
|
|
212
|
+
sig_path.write_text(
|
|
213
|
+
"untrusted comment: placeholder - real signature added by CI\n"
|
|
214
|
+
f"manifest_sha256_self: {manifest.manifest_sha256_self}\n",
|
|
215
|
+
encoding="utf-8",
|
|
216
|
+
)
|
|
217
|
+
return manifest_path, sig_path
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Validation helpers (used by tests + postinstall)
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def validate_shape(doc: dict) -> list[str]:
|
|
225
|
+
"""Return a list of validation errors on a loaded manifest dict."""
|
|
226
|
+
errors: list[str] = []
|
|
227
|
+
for key in ("version", "released_at", "assets",
|
|
228
|
+
"manifest_sha256_self", "manifest_signature"):
|
|
229
|
+
if key not in doc:
|
|
230
|
+
errors.append(f"missing top-level key: {key}")
|
|
231
|
+
|
|
232
|
+
assets = doc.get("assets")
|
|
233
|
+
if not isinstance(assets, list):
|
|
234
|
+
errors.append("assets must be a list")
|
|
235
|
+
return errors
|
|
236
|
+
if not assets:
|
|
237
|
+
errors.append("assets list is empty")
|
|
238
|
+
|
|
239
|
+
for i, entry in enumerate(assets):
|
|
240
|
+
if not isinstance(entry, dict):
|
|
241
|
+
errors.append(f"asset[{i}] must be an object")
|
|
242
|
+
continue
|
|
243
|
+
for field_name in ("name", "url", "size_bytes",
|
|
244
|
+
"sha256", "signing"):
|
|
245
|
+
if field_name not in entry:
|
|
246
|
+
errors.append(
|
|
247
|
+
f"asset[{i}] missing field: {field_name}"
|
|
248
|
+
)
|
|
249
|
+
sha = entry.get("sha256", "")
|
|
250
|
+
if not isinstance(sha, str) or len(sha) != 64:
|
|
251
|
+
errors.append(f"asset[{i}] sha256 must be 64-char hex")
|
|
252
|
+
signing = entry.get("signing")
|
|
253
|
+
if signing is not None:
|
|
254
|
+
try:
|
|
255
|
+
_validate_signing(signing)
|
|
256
|
+
except ValueError as exc:
|
|
257
|
+
errors.append(f"asset[{i}] {exc}")
|
|
258
|
+
return errors
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = (
|
|
262
|
+
"AssetSpec",
|
|
263
|
+
"Manifest",
|
|
264
|
+
"SIGNING_APPLE_NOTARIZED",
|
|
265
|
+
"SIGNING_AUTHENTICODE",
|
|
266
|
+
"SIGNING_UNSIGNED",
|
|
267
|
+
"DEFAULT_BASE_URL",
|
|
268
|
+
"build_asset_entry",
|
|
269
|
+
"build_manifest",
|
|
270
|
+
"serialize",
|
|
271
|
+
"write_manifest",
|
|
272
|
+
"validate_shape",
|
|
273
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- mode: python ; coding: utf-8 -*-
|
|
2
|
+
# LLD reference: .backup/active-brain/lld/LLD-06-windows-binary-and-legacy-migration.md §4.2
|
|
3
|
+
# Mode: onedir (not onefile) — LLD-06 §4.1, verification claim 7.
|
|
4
|
+
# console=False, upx=False, strip=True.
|
|
5
|
+
|
|
6
|
+
block_cipher = None
|
|
7
|
+
|
|
8
|
+
a = Analysis(
|
|
9
|
+
['../src/superlocalmemory/hook_binary_entry.py'],
|
|
10
|
+
pathex=[],
|
|
11
|
+
binaries=[],
|
|
12
|
+
datas=[],
|
|
13
|
+
hiddenimports=[],
|
|
14
|
+
hookspath=[],
|
|
15
|
+
runtime_hooks=[],
|
|
16
|
+
excludes=[
|
|
17
|
+
# Deny-list — anything imported here would blow up cold-start and size.
|
|
18
|
+
'torch', 'sentence_transformers', 'fastapi', 'uvicorn',
|
|
19
|
+
'numpy', 'scipy', 'lightgbm', 'onnxruntime', 'transformers',
|
|
20
|
+
'httpx', 'mcp', 'pydantic', 'lark',
|
|
21
|
+
'superlocalmemory.core.engine',
|
|
22
|
+
'superlocalmemory.core.embeddings',
|
|
23
|
+
'superlocalmemory.retrieval',
|
|
24
|
+
'superlocalmemory.learning',
|
|
25
|
+
],
|
|
26
|
+
cipher=block_cipher,
|
|
27
|
+
noarchive=False,
|
|
28
|
+
)
|
|
29
|
+
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|
30
|
+
|
|
31
|
+
exe = EXE(
|
|
32
|
+
pyz,
|
|
33
|
+
a.scripts,
|
|
34
|
+
[],
|
|
35
|
+
exclude_binaries=True,
|
|
36
|
+
name='slm-hook',
|
|
37
|
+
debug=False,
|
|
38
|
+
bootloader_ignore_signals=False,
|
|
39
|
+
strip=True,
|
|
40
|
+
upx=False, # UPX adds 30-60 ms cold-start + AV heuristics
|
|
41
|
+
console=False, # no console flash on Windows
|
|
42
|
+
disable_windowed_traceback=False,
|
|
43
|
+
target_arch=None,
|
|
44
|
+
codesign_identity=None, # handled in scripts/sign-macos.sh post-build
|
|
45
|
+
entitlements_file=None,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
coll = COLLECT(
|
|
49
|
+
exe,
|
|
50
|
+
a.binaries,
|
|
51
|
+
a.zipfiles,
|
|
52
|
+
a.datas,
|
|
53
|
+
strip=True,
|
|
54
|
+
upx=False,
|
|
55
|
+
name='slm-hook',
|
|
56
|
+
)
|