@getmarrow/install 0.1.68 → 0.1.70

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/src/owner-env.js CHANGED
@@ -71,7 +71,8 @@ function readOwnerApiKey(home) {
71
71
  if (!fs.existsSync(filePath) || !isPrivateOwnerFile(filePath, home)) continue;
72
72
  try {
73
73
  const values = parseKeyLines(fs.readFileSync(filePath, 'utf8'));
74
- const apiKey = values.MARROW_API_KEY || values.MARROW_KEY || '';
74
+ // Blank (quoted whitespace) values are absent: they never shadow a valid key in another file.
75
+ const apiKey = String(values.MARROW_API_KEY || '').trim() || String(values.MARROW_KEY || '').trim();
75
76
  if (apiKey) found.push({ apiKey, source: filePath });
76
77
  } catch {
77
78
  // An unreadable owner file is treated as absent.
package/src/pins.js CHANGED
@@ -2,9 +2,9 @@
2
2
  // This is the only place they are set: a release changes the MCP version, source SHA and
3
3
  // integrity here together (and the SDK pair together), and the tests read them from here.
4
4
  module.exports = Object.freeze({
5
- MCP_ADAPTER_VERSION: '3.9.100',
6
- MCP_ADAPTER_SOURCE_SHA: 'dfe8ca59b7862c481443876f5b1d0f453d244a44',
7
- MCP_ADAPTER_INTEGRITY: 'sha512-6YfkDSrYhDwjTUBAo7toJ808R1pzxefjX5Dk3fq8hVfr8FrR+DMLbZTHOM0vU1uhPOp8H5GFIniTWTNh2/5Sig==',
5
+ MCP_ADAPTER_VERSION: '3.9.102',
6
+ MCP_ADAPTER_SOURCE_SHA: '3a2bf85e22b6b9f7e266383ed2011b1c10ca5f97',
7
+ MCP_ADAPTER_INTEGRITY: 'sha512-hTs4jTJmySLk8hHgsGIZksMo9+eO5kjQmU1EJyDX8Qqm9Z+V+yDkauop19roasVvOAFsK3dJjqLoIh7gRhoXJQ==',
8
8
  // The first MCP version that ships the host-approval hook entrypoints
9
9
  // (claude-permission-request-hook, cursor-context-hook, gemini-context-hook) and reads Cursor's
10
10
  // shell/MCP execution events and Gemini's BeforeAgent prompt. The installer writes the
@@ -0,0 +1,594 @@
1
+ 'use strict';
2
+
3
+ // The one implementation of Marrow's private MCP runtime: verifying a copy, the private install,
4
+ // and the self-heal a hook runs when its verified copy is missing or changed. The installer calls
5
+ // it in process. The same function text is the loader every hook command carries inline (see
6
+ // loaderFor in mcp-runtime.js), so a hook on a machine with no ~/.marrow at all installs the
7
+ // pinned copy with exactly the installer's rules, and never reads the project or starts npx.
8
+ //
9
+ // Because its text is shipped inside hook commands, runtimeCore must stay self-contained: node:
10
+ // builtins only, no outer variables, no comments and no string spanning lines inside it.
11
+ //
12
+ // Install rules: npm runs as `node <npm-cli.js next to node>` (Windows: only that; POSIX: `npm`
13
+ // found on PATH as the hooks find node, never a relative entry or one inside the project, when no
14
+ // npm-cli.js exists), shell:false, --ignore-scripts, --no-bin-links, an explicit
15
+ // private --prefix stage that is also the working directory, every npm-owned npm_* variable removed except
16
+ // an exact allowlist of network settings (registry, proxy, CA) from npm_config_* env, and the
17
+ // pinned sha512 integrity, version and entry checked before the stage is renamed into place. When
18
+ // a network setting is passed, the install must be exactly the pinned MCP and SDK, each with its
19
+ // pinned integrity, so the registry it names can only serve the pinned bytes.
20
+ // npm auth (FU-1): the owner's npm config, and so their auth, is used only when no network
21
+ // setting comes from outside it: none passed from env, none refused as unparseable, no proxy, CA,
22
+ // TLS, DNS, OpenSSL or PREFIX variable that npm or node reads, and npm resolving the same network
23
+ // settings and the same config key names under the hook env as under a bare one (no env-expanded
24
+ // value, except an auth value, which npm shows only as (protected); a certfile or keyfile, whose
25
+ // value npm never shows, or a redacted value counts as moved). Only a no-proxy setting (it can only
26
+ // remove proxying) and a NODE_OPTIONS made only of a short allowlist of memory and warning flags do
27
+ // not count as from outside. npm-owned npm_* variables are removed; others reach npm, so an
28
+ // owner auth value of ${VAR} form expands. Otherwise npm reads
29
+ // private user and global configs (removed after) that carry no auth, proxy, CA or TLS setting:
30
+ // the global one is empty and the user one holds only the owner's registry and @getmarrow:registry
31
+ // URLs, taken from `npm config get` under a bare env (read twice, equal), each refused when it has
32
+ // userinfo, a control character, a ${ , a ? # or *, is over 2048 characters or is not http(s). When any proxy,
33
+ // CA, TLS, DNS, OpenSSL or PREFIX setting comes from outside (not only an env registry), or the
34
+ // config moved, a URL whose path is not exactly / is refused too (a path can carry a token). An env
35
+ // registry keeps npm's precedence (env over user config), exactly as before, and gets no owner
36
+ // data. The pinned public tarballs need no auth. A project npm config is never read (the stage is
37
+ // the prefix and working dir).
38
+ // Self-heal: one lock per version (a directory created atomically), a detached child that does
39
+ // the install so a host timeout cannot kill it half way, a bounded wait in the hook, and a
40
+ // failure record per project (keyed by the sha256 of the project's real path) so an offline
41
+ // machine answers at once, while still starting a heal at most once per 30 s per project.
42
+ // Outage rule: when the heal fails for an outage (no npm, npm failed, the wait ran out), a
43
+ // pre-action hook warns and lets the action run (exit 1, as npx failing did before); any other
44
+ // failure (a tarball whose integrity does not match, an unsafe directory or package) holds it.
45
+ // No copy that is not verified ever runs in either case.
46
+ function runtimeCore() {
47
+ const fs = require('node:fs');
48
+ const path = require('node:path');
49
+ const crypto = require('node:crypto');
50
+ const cp = () => require('node:child_process');
51
+ const SCHEMA = 'marrow-mcp-runtime.v2';
52
+ const VERSION_RE = /^\d+\.\d+\.\d+$/;
53
+ const INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/;
54
+ const SAFE_PATH_RE = /^[^\n\r\0]+$/;
55
+ const NPM_TIMEOUT_MS = 30000;
56
+ const STALE_MS = 300000;
57
+ const LOCK_STALE_MS = 120000;
58
+ const RETRY_MS = 300000;
59
+ const ATTEMPT_MS = 30000;
60
+ const NETWORK_KEYS = ['registry', '@getmarrow:registry', 'proxy', 'https_proxy', 'noproxy', 'cafile', 'ca'];
61
+ const CONFIG_KEYS = ['registry', '@getmarrow:registry', 'proxy', 'https-proxy', 'noproxy', 'cafile', 'ca', 'strict-ssl', 'local-address', 'replace-registry-host', 'userconfig', 'globalconfig'];
62
+ const OWNER_KEYS = ['registry', '@getmarrow:registry'];
63
+ const FOREIGN_CONFIG = ['strict_ssl', 'local_address', 'replace_registry_host'];
64
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
65
+ const BASE_ENV = ['path', 'home', 'userprofile', 'appdata', 'localappdata', 'systemroot', 'systemdrive', 'windir', 'comspec', 'pathext', 'temp', 'tmp', 'tmpdir'];
66
+ const OUTSIDE_ENV = ['http_proxy', 'https_proxy', 'proxy', 'all_proxy', 'node_extra_ca_certs', 'node_tls_reject_unauthorized', 'node_use_env_proxy', 'ssl_cert_file', 'ssl_cert_dir', 'openssl_conf', 'openssl_modules', 'hostaliases', 'localdomain', 'res_options', 'prefix', 'destdir'];
67
+ const NODE_OPTION_RE = /^(--max-old-space-size=\d{1,6}|--max-semi-space-size=\d{1,4}|--enable-source-maps|--no-warnings|--no-deprecation|--trace-warnings|--trace-deprecation)$/;
68
+ const NPM_OWN_RE = /^npm_(config|lifecycle_|package_|command$|execpath$|node_execpath$)/i;
69
+ const PRE_RE = /-pre-action-hook$/;
70
+ const REPAIR = 'The owner can repair it by running npx -y @getmarrow/install@latest --repair --cwd <project folder> from a terminal outside the project.';
71
+ const HOLD = 'Marrow held this action: its verified local runtime could not start. ' + REPAIR;
72
+ const UNAVAILABLE = 'Marrow could not start its verified local runtime. ' + REPAIR;
73
+ const WARN = 'Marrow warning: it could not load its verified runtime, so this action was not checked. ' + REPAIR;
74
+ const OUTAGE = ['npm_unavailable', 'npm_install_failed', 'heal_timeout'];
75
+ const uid = () => (typeof process.getuid === 'function' ? process.getuid() : null);
76
+ const rootOf = (home) => path.join(home, '.marrow', 'runtime', 'mcp');
77
+ const dirOf = (home, version) => path.join(rootOf(home), version);
78
+ const byName = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
79
+ const sdkPin = (sdk) => (sdk && VERSION_RE.test(String(sdk.version || '')) && INTEGRITY_RE.test(String(sdk.integrity || ''))
80
+ ? { version: sdk.version, integrity: sdk.integrity } : null);
81
+ const configOf = (version, integrity, sdk) => ({ version: String(version || ''), integrity: INTEGRITY_RE.test(String(integrity || '')) ? integrity : null, sdk: sdkPin(sdk) });
82
+ function removeQuietly(target) {
83
+ try {
84
+ fs.rmSync(target, { recursive: true, force: true });
85
+ } catch {
86
+ return false;
87
+ }
88
+ return true;
89
+ }
90
+ function ensurePrivateDirectory(directory) {
91
+ try {
92
+ fs.mkdirSync(directory, { mode: 0o700 });
93
+ } catch (error) {
94
+ if (!error || error.code !== 'EEXIST') throw error;
95
+ }
96
+ const stat = fs.lstatSync(directory);
97
+ const me = uid();
98
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (me !== null && stat.uid !== me)) throw new Error('unsafe runtime directory ' + directory);
99
+ if ((stat.mode & 0o077) !== 0) fs.chmodSync(directory, 0o700);
100
+ }
101
+ function ensurePrivateRoot(home) {
102
+ ensurePrivateDirectory(path.join(home, '.marrow'));
103
+ ensurePrivateDirectory(path.dirname(rootOf(home)));
104
+ ensurePrivateDirectory(rootOf(home));
105
+ }
106
+ function privateDirectoryOk(directory, platform) {
107
+ try {
108
+ const stat = fs.lstatSync(directory);
109
+ const me = uid();
110
+ return stat.isDirectory() && !stat.isSymbolicLink() && (me === null || stat.uid === me) && (platform === 'win32' || (stat.mode & 0o077) === 0);
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+ function listFiles(base, relative, platform) {
116
+ const out = [];
117
+ for (const name of fs.readdirSync(path.join(base, relative)).sort(byName)) {
118
+ const rel = relative ? relative + '/' + name : name;
119
+ if (rel === 'node_modules/.bin') continue;
120
+ const stat = fs.lstatSync(path.join(base, rel));
121
+ if (platform !== 'win32' && (stat.mode & 0o002) !== 0) throw new Error('world-writable entry in runtime package: ' + rel);
122
+ if (stat.isDirectory()) out.push(...listFiles(base, rel, platform));
123
+ else if (stat.isFile()) out.push(rel);
124
+ else throw new Error('unexpected entry in runtime package: ' + rel);
125
+ }
126
+ return out;
127
+ }
128
+ function packageFiles(directory, platform) {
129
+ if (!fs.lstatSync(path.join(directory, 'node_modules')).isDirectory()) throw new Error('runtime node_modules is not a directory');
130
+ return listFiles(directory, 'node_modules', platform).filter((file) => file !== 'node_modules/.package-lock.json');
131
+ }
132
+ function manifestDigest(directory, files) {
133
+ const hash = crypto.createHash('sha256');
134
+ for (const file of files) {
135
+ hash.update(file);
136
+ hash.update(Buffer.alloc(1));
137
+ hash.update(fs.readFileSync(path.join(directory, file)));
138
+ hash.update(Buffer.alloc(1));
139
+ }
140
+ return hash.digest('hex');
141
+ }
142
+ function readRecord(directory, platform) {
143
+ try {
144
+ const stat = fs.lstatSync(path.join(directory, '.verified'));
145
+ const me = uid();
146
+ if (!stat.isFile() || (me !== null && stat.uid !== me) || (platform !== 'win32' && (stat.mode & 0o077) !== 0)) return null;
147
+ const record = JSON.parse(fs.readFileSync(path.join(directory, '.verified'), 'utf8'));
148
+ return record && record.schema === SCHEMA ? record : null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function verify(home, version, integrity, options) {
154
+ const platform = (options && options.platform) || process.platform;
155
+ if (!home || !VERSION_RE.test(String(version || '')) || !INTEGRITY_RE.test(String(integrity || ''))) return null;
156
+ const directory = dirOf(home, version);
157
+ const dirs = [path.join(home, '.marrow'), path.dirname(rootOf(home)), rootOf(home), directory];
158
+ if (!dirs.every((dir) => privateDirectoryOk(dir, platform))) return null;
159
+ const record = readRecord(directory, platform);
160
+ if (!record || record.version !== version || record.integrity !== integrity) return null;
161
+ if (typeof record.bin !== 'string' || !Array.isArray(record.files) || !record.files.includes(record.bin)) return null;
162
+ try {
163
+ const files = packageFiles(directory, platform);
164
+ if (JSON.stringify(files) !== JSON.stringify(record.files)) return null;
165
+ if (manifestDigest(directory, files) !== record.manifest_sha256) return null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ const result = { state: 'verified', version, directory, bin: path.join(directory, record.bin) };
170
+ return platform === 'win32' ? result : { ...result, node: path.join(directory, 'node') };
171
+ }
172
+ function npmInvocation(options) {
173
+ const platform = (options && options.platform) || process.platform;
174
+ const execPath = (options && options.execPath) || process.execPath;
175
+ const bin = path.dirname(execPath);
176
+ const candidates = platform === 'win32'
177
+ ? [path.join(bin, 'node_modules', 'npm', 'bin', 'npm-cli.js')]
178
+ : [path.join(bin, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), '/usr/share/nodejs/npm/bin/npm-cli.js', '/usr/lib/node_modules/npm/bin/npm-cli.js'];
179
+ for (const cli of candidates) {
180
+ try {
181
+ if (fs.statSync(cli).isFile()) return { file: execPath, args: [cli] };
182
+ } catch {
183
+ continue;
184
+ }
185
+ }
186
+ if (platform === 'win32') return null;
187
+ const env = (options && options.env) || process.env;
188
+ const project = [String(env.PWD || process.cwd())];
189
+ try {
190
+ project.push(fs.realpathSync(project[0]));
191
+ } catch {
192
+ project.push(project[0]);
193
+ }
194
+ const inside = (dir) => project.some((root) => !path.isAbsolute(root) || dir === root || dir.startsWith(root.replace(/\/+$/, '') + '/'));
195
+ const dirs = [];
196
+ for (const dir of [...String(env.PATH || '').split(':'), '/usr/local/bin', '/opt/homebrew/bin']) {
197
+ if (!path.isAbsolute(dir) || inside(dir)) continue;
198
+ try {
199
+ if (inside(fs.realpathSync(dir))) continue;
200
+ } catch {
201
+ continue;
202
+ }
203
+ dirs.push(dir);
204
+ }
205
+ for (const dir of dirs) {
206
+ try {
207
+ const file = path.join(dir, 'npm');
208
+ const real = fs.realpathSync(file);
209
+ const stat = fs.statSync(real);
210
+ if (!stat.isFile() || (stat.mode & 0o111) === 0 || inside(real)) continue;
211
+ return /\.js$/.test(real) ? { file: execPath, args: [real], path: dirs.join(':') } : { file: real, args: [], path: dirs.join(':') };
212
+ } catch {
213
+ continue;
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ function npmNetwork(source) {
219
+ const out = {};
220
+ const env = source || process.env;
221
+ for (const key of Object.keys(env).sort(byName)) {
222
+ const match = /^npm_config_(.+)$/i.exec(key);
223
+ if (!match) continue;
224
+ const name = match[1].toLowerCase().replace(/-/g, '_');
225
+ const value = env[key];
226
+ if (!NETWORK_KEYS.includes(name) || typeof value !== 'string' || !value || value.length > 2048 || /[\n\r\0]|\$\{/.test(value)) continue;
227
+ out['npm_config_' + name] = value;
228
+ }
229
+ return out;
230
+ }
231
+ function outsideName(key, value) {
232
+ if (key.toLowerCase() === 'node_options') return value !== undefined && value !== '' && !String(value).split(' ').every((token) => NODE_OPTION_RE.test(token));
233
+ return OUTSIDE_ENV.includes(key.toLowerCase());
234
+ }
235
+ function npmOutside(source) {
236
+ const env = source || process.env;
237
+ return Object.keys(env).some((key) => {
238
+ const match = /^npm_config_(.+)$/i.exec(key);
239
+ return outsideName(key, env[key]) || Boolean(match && NETWORK_KEYS.includes(match[1].toLowerCase().replace(/-/g, '_')) && !Object.keys(npmNetwork({ [key]: env[key] })).length);
240
+ });
241
+ }
242
+ function bareEnv(env) {
243
+ const base = { npm_config_ignore_scripts: 'true', npm_config_update_notifier: 'false', npm_config_audit: 'false', npm_config_fund: 'false' };
244
+ for (const key of Object.keys(env)) if (BASE_ENV.includes(key.toLowerCase()) || key === 'npm_config_noproxy') base[key] = env[key];
245
+ return base;
246
+ }
247
+ function npmRead(npm, stage, vars, keys, timeout) {
248
+ const done = cp().spawnSync(npm.file, [...npm.args, 'config', ...(keys ? ['get', ...keys] : ['list']), '--prefix=' + stage], { cwd: stage, env: npm.path ? { ...vars, PATH: npm.path } : vars, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 20, timeout: Math.min(timeout, 10000), windowsHide: true, shell: false });
249
+ return done.status === 0 ? String(done.stdout) : null;
250
+ }
251
+ function npmConfigMoves(npm, stage, env, timeout) {
252
+ const full = npmRead(npm, stage, env, CONFIG_KEYS, timeout);
253
+ const plain = npmRead(npm, stage, bareEnv(env), CONFIG_KEYS, timeout);
254
+ if (full === null || plain === null || full !== plain) return true;
255
+ const list = npmRead(npm, stage, env, null, timeout);
256
+ const plainList = npmRead(npm, stage, bareEnv(env), null, timeout);
257
+ return list === null || plainList === null || list !== plainList || list.includes('***') || list.split('\n').some((line) => /^(\/\/\S*:)?_?(certfile|keyfile) = /i.test(line));
258
+ }
259
+ function npmForeign(source) {
260
+ const env = source || process.env;
261
+ return Object.keys(env).some((key) => {
262
+ if (outsideName(key, env[key])) return true;
263
+ const match = /^npm_config_(.+)$/i.exec(key);
264
+ if (!match) return false;
265
+ const name = match[1].toLowerCase().replace(/-/g, '_');
266
+ if (name === 'registry' || name === '@getmarrow:registry' || name === 'noproxy') return Object.keys(npmNetwork({ [key]: env[key] })).length === 0;
267
+ return NETWORK_KEYS.includes(name) || FOREIGN_CONFIG.includes(name);
268
+ });
269
+ }
270
+ function ownerUrl(value, rootOnly) {
271
+ if (typeof value !== 'string' || value.length > 2048 || /[^\x21-\x7e]|["'\\#;`?*]|\$\{/.test(value) || !/^https?:\/\/[^/?]+/i.test(value)) return null;
272
+ if (/^https?:\/\/[^/?]*@/i.test(value)) return null;
273
+ if (rootOnly && !/^https?:\/\/[^/]+\/?$/i.test(value)) return null;
274
+ try {
275
+ const url = new URL(value);
276
+ return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname && !url.username && !url.password ? value : null;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ function npmOwnerRegistries(npm, stage, env, timeout, rootOnly) {
282
+ const first = npmRead(npm, stage, bareEnv(env), OWNER_KEYS, timeout);
283
+ const second = first === null ? null : npmRead(npm, stage, bareEnv(env), OWNER_KEYS, timeout);
284
+ const out = {};
285
+ if (first === null || first !== second) return out;
286
+ const lines = first.replace(/\n$/, '').split('\n');
287
+ if (lines.length !== OWNER_KEYS.length) return out;
288
+ OWNER_KEYS.forEach((key, index) => {
289
+ const value = lines[index].startsWith(key + '=') ? ownerUrl(lines[index].slice(key.length + 1), rootOnly) : null;
290
+ if (value && value.replace(/\/+$/, '') !== DEFAULT_REGISTRY) out[key] = value;
291
+ });
292
+ return out;
293
+ }
294
+ function npmEnvironment(source, network) {
295
+ const env = {};
296
+ for (const [key, value] of Object.entries(source || process.env)) {
297
+ if (NPM_OWN_RE.test(key)) continue;
298
+ env[key] = value;
299
+ }
300
+ return { ...env, ...(network || {}), npm_config_ignore_scripts: 'true', npm_config_update_notifier: 'false', npm_config_audit: 'false', npm_config_fund: 'false' };
301
+ }
302
+ function npmInstall(stage, specs, options) {
303
+ const npm = npmInvocation(options);
304
+ if (!npm) return 'npm_unavailable';
305
+ const timeout = Math.max(1000, Math.min(NPM_TIMEOUT_MS, Number((options && options.timeout) || NPM_TIMEOUT_MS)));
306
+ const source = (options && options.env) || process.env;
307
+ const env = npmEnvironment(source, options && options.network);
308
+ const passed = Object.keys((options && options.network) || {}).some((key) => key !== 'npm_config_noproxy') || npmOutside(source);
309
+ const moved = !passed && npmConfigMoves(npm, stage, env, timeout);
310
+ const blank = passed || moved ? stage + '-npmrc' : null;
311
+ const owner = blank ? npmOwnerRegistries(npm, stage, env, timeout, moved || npmForeign(source)) : {};
312
+ let result;
313
+ try {
314
+ if (blank) {
315
+ fs.mkdirSync(blank, { mode: 0o700 });
316
+ fs.writeFileSync(path.join(blank, 'user'), Object.keys(owner).map((key) => key + '=' + owner[key] + '\n').join(''), { mode: 0o600, flag: 'wx' });
317
+ fs.writeFileSync(path.join(blank, 'global'), '', { mode: 0o600, flag: 'wx' });
318
+ }
319
+ const config = blank ? ['--userconfig=' + path.join(blank, 'user'), '--globalconfig=' + path.join(blank, 'global')] : [];
320
+ result = cp().spawnSync(npm.file, [...npm.args, 'install', '--no-audit', '--no-fund', '--ignore-scripts', '--no-bin-links', '--loglevel=error', '--prefix=' + stage, ...config, ...specs], {
321
+ cwd: stage,
322
+ env: npm.path ? { ...env, PATH: npm.path } : env,
323
+ stdio: ['ignore', 'ignore', 'pipe'],
324
+ maxBuffer: 1 << 20,
325
+ timeout,
326
+ windowsHide: true,
327
+ shell: false,
328
+ });
329
+ } finally {
330
+ if (blank) removeQuietly(blank);
331
+ }
332
+ if (result.status === 0) return true;
333
+ return /EINTEGRITY/.test(String(result.stderr || '')) ? 'integrity_mismatch' : false;
334
+ }
335
+ function removeOtherRuntimes(home, keep) {
336
+ const root = rootOf(home);
337
+ const removed = [];
338
+ let entries = [];
339
+ try {
340
+ entries = fs.readdirSync(root);
341
+ } catch {
342
+ return removed;
343
+ }
344
+ for (const entry of entries) {
345
+ if (entry === keep) continue;
346
+ if (entry.startsWith('.stage-') || entry.startsWith('.heal-')) {
347
+ try {
348
+ if (Date.now() - fs.lstatSync(path.join(root, entry)).mtimeMs < STALE_MS) continue;
349
+ } catch {
350
+ continue;
351
+ }
352
+ }
353
+ removeQuietly(path.join(root, entry));
354
+ removed.push(entry);
355
+ }
356
+ return removed;
357
+ }
358
+ function ensure(options) {
359
+ const { home, version, integrity, sdk = null, nodePath = process.execPath, npmExecPath = process.execPath, onProgress = null, prune = true } = options || {};
360
+ const platform = (options && options.platform) || process.platform;
361
+ const install = (options && options.npmInstall) || npmInstall;
362
+ const windows = platform === 'win32';
363
+ if (!home || !VERSION_RE.test(String(version || '')) || !INTEGRITY_RE.test(String(integrity || ''))) return { state: 'skipped', reason: 'no_verified_integrity' };
364
+ const pruned = () => (prune ? removeOtherRuntimes(home, version) : []);
365
+ const existing = verify(home, version, integrity, { platform });
366
+ if (existing) return { ...existing, state: 'present', removed_versions: pruned() };
367
+ const directory = dirOf(home, version);
368
+ if (!windows && (!SAFE_PATH_RE.test(directory) || !SAFE_PATH_RE.test(String(nodePath || '')) || !path.isAbsolute(String(nodePath || '')))) return { state: 'skipped', reason: 'unsafe_path' };
369
+ let stage = null;
370
+ try {
371
+ ensurePrivateRoot(home);
372
+ stage = path.join(rootOf(home), '.stage-' + version + '-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
373
+ fs.mkdirSync(stage, { mode: 0o700 });
374
+ const dependencies = { '@getmarrow/mcp': version };
375
+ const pin = sdkPin(sdk);
376
+ if (sdk && VERSION_RE.test(String(sdk.version || ''))) dependencies['@getmarrow/sdk'] = sdk.version;
377
+ fs.writeFileSync(path.join(stage, 'package.json'), JSON.stringify({ name: 'marrow-mcp-runtime', private: true, dependencies }, null, 2) + '\n', { mode: 0o600 });
378
+ const specs = Object.entries(dependencies).map(([name, spec]) => name + '@' + spec);
379
+ if (typeof onProgress === 'function') onProgress('Installing a local copy of @getmarrow/mcp@' + version + ' for Marrow\'s hooks (up to ' + NPM_TIMEOUT_MS / 1000 + ' s; until it is installed, hooks install it themselves, warn and let actions run when npm or the registry cannot be reached, and hold actions on any other failure)...\n');
380
+ const network = pin ? npmNetwork((options && options.env) || process.env) : {};
381
+ const strict = Object.keys(network).length > 0;
382
+ const installed = install(stage, specs, { platform, execPath: npmExecPath, timeout: options && options.timeout, env: options && options.env, network });
383
+ if (!installed || typeof installed === 'string') return { state: 'failed', reason: installed || 'npm_install_failed' };
384
+ const lock = JSON.parse(fs.readFileSync(path.join(stage, 'package-lock.json'), 'utf8'));
385
+ const packages = lock && typeof lock.packages === 'object' && lock.packages ? lock.packages : {};
386
+ const mcp = packages['node_modules/@getmarrow/mcp'] || {};
387
+ if (mcp.version !== version || mcp.integrity !== integrity) return { state: 'failed', reason: 'integrity_mismatch' };
388
+ const sdkEntry = packages['node_modules/@getmarrow/sdk'];
389
+ if (sdk && sdkEntry && sdkEntry.version === sdk.version && sdkEntry.integrity !== sdk.integrity) return { state: 'failed', reason: 'sdk_integrity_mismatch' };
390
+ for (const [key, entry] of Object.entries(packages)) {
391
+ if (key === '') continue;
392
+ if (!entry || typeof entry.integrity !== 'string' || !/^sha512-/.test(entry.integrity)) return { state: 'failed', reason: 'unverified_dependency' };
393
+ }
394
+ if (strict && (Object.keys(packages).filter((key) => key !== '').sort(byName).join('\n') !== 'node_modules/@getmarrow/mcp\nnode_modules/@getmarrow/sdk' || !sdkEntry || sdkEntry.version !== pin.version || sdkEntry.integrity !== pin.integrity)) return { state: 'failed', reason: 'unverified_dependency' };
395
+ const manifest = JSON.parse(fs.readFileSync(path.join(stage, 'node_modules', '@getmarrow', 'mcp', 'package.json'), 'utf8'));
396
+ if (manifest.name !== '@getmarrow/mcp' || manifest.version !== version) return { state: 'failed', reason: 'package_mismatch' };
397
+ const binRelative = typeof manifest.bin === 'object' && manifest.bin ? manifest.bin['marrow-mcp'] : null;
398
+ if (typeof binRelative !== 'string' || !/^[A-Za-z0-9._/-]+$/.test(binRelative) || binRelative.includes('..')) return { state: 'failed', reason: 'package_entry_missing' };
399
+ const bin = 'node_modules/@getmarrow/mcp/' + binRelative.replace(/^\.\//, '');
400
+ let files;
401
+ try {
402
+ files = packageFiles(stage, platform);
403
+ } catch {
404
+ return { state: 'failed', reason: 'unsafe_package_entry' };
405
+ }
406
+ if (!files.includes(bin)) return { state: 'failed', reason: 'package_entry_missing' };
407
+ if (strict && !files.every((file) => /^node_modules\/@getmarrow\/(mcp|sdk)\//.test(file) && !file.slice('node_modules/@getmarrow/'.length).includes('node_modules/'))) return { state: 'failed', reason: 'unverified_dependency' };
408
+ if (!windows) fs.symlinkSync(nodePath, path.join(stage, 'node'));
409
+ fs.writeFileSync(path.join(stage, '.verified'), JSON.stringify({
410
+ schema: SCHEMA,
411
+ version,
412
+ integrity,
413
+ sdk_pin: pin,
414
+ sdk_version: sdkEntry ? sdkEntry.version : null,
415
+ bin,
416
+ files,
417
+ manifest_sha256: manifestDigest(stage, files),
418
+ installed_at: new Date().toISOString(),
419
+ }, null, 2) + '\n', { mode: 0o600 });
420
+ const raced = verify(home, version, integrity, { platform });
421
+ if (raced) return { ...raced, state: 'present', removed_versions: pruned() };
422
+ removeQuietly(directory);
423
+ try {
424
+ fs.renameSync(stage, directory);
425
+ } catch (error) {
426
+ const other = verify(home, version, integrity, { platform });
427
+ if (other) return { ...other, state: 'present', removed_versions: pruned() };
428
+ throw error;
429
+ }
430
+ stage = null;
431
+ const verified = verify(home, version, integrity, { platform });
432
+ if (!verified) return { state: 'failed', reason: 'verification_failed' };
433
+ return { ...verified, state: 'installed', removed_versions: pruned() };
434
+ } catch (error) {
435
+ return { state: 'failed', reason: error && /unsafe runtime directory/.test(error.message) ? 'unsafe_directory' : 'install_error' };
436
+ } finally {
437
+ if (stage) removeQuietly(stage);
438
+ }
439
+ }
440
+ function projectKey(project) {
441
+ let real = String(project || process.cwd());
442
+ try {
443
+ real = fs.realpathSync(real);
444
+ } catch {
445
+ real = path.resolve(real);
446
+ }
447
+ return crypto.createHash('sha256').update(real).digest('hex');
448
+ }
449
+ function healPaths(home, version, project) {
450
+ const key = projectKey(project);
451
+ return { lock: path.join(rootOf(home), '.heal-' + version + '.lock'), mark: path.join(rootOf(home), '.heal-' + version + '-' + key + '.failed'), tried: path.join(rootOf(home), '.heal-' + version + '-' + key + '.tried') };
452
+ }
453
+ function failure(home, mark) {
454
+ const none = { at: 0, reason: '' };
455
+ try {
456
+ const platform = process.platform;
457
+ if (![path.join(home, '.marrow'), path.dirname(rootOf(home)), rootOf(home)].every((dir) => privateDirectoryOk(dir, platform))) return none;
458
+ const stat = fs.lstatSync(mark);
459
+ const me = uid();
460
+ if (!stat.isFile() || (me !== null && stat.uid !== me) || (platform !== 'win32' && (stat.mode & 0o077) !== 0)) return none;
461
+ const record = JSON.parse(fs.readFileSync(mark, 'utf8'));
462
+ const at = Number(record.at) || 0;
463
+ return at > Date.now() + 5000 ? none : { at, reason: String(record.reason || '') };
464
+ } catch {
465
+ return none;
466
+ }
467
+ }
468
+ function lockBusy(lock) {
469
+ try {
470
+ return Date.now() - fs.lstatSync(lock).mtimeMs < LOCK_STALE_MS;
471
+ } catch {
472
+ return false;
473
+ }
474
+ }
475
+ function heal(cfg, home, options) {
476
+ if (!home || !VERSION_RE.test(cfg.version) || !INTEGRITY_RE.test(String(cfg.integrity || ''))) return { state: 'skipped', reason: 'no_verified_integrity' };
477
+ const { lock, mark, tried } = healPaths(home, cfg.version, (options && options.project) || process.env.PWD || process.cwd());
478
+ try {
479
+ ensurePrivateRoot(home);
480
+ } catch {
481
+ return { state: 'failed', reason: 'unsafe_directory' };
482
+ }
483
+ try {
484
+ fs.mkdirSync(lock, { mode: 0o700 });
485
+ } catch (error) {
486
+ if (!error || error.code !== 'EEXIST' || lockBusy(lock)) return { state: 'busy' };
487
+ removeQuietly(lock);
488
+ try {
489
+ fs.mkdirSync(lock, { mode: 0o700 });
490
+ } catch {
491
+ return { state: 'busy' };
492
+ }
493
+ }
494
+ try {
495
+ if (Date.now() - failure(home, tried).at < ATTEMPT_MS) return { state: 'busy' };
496
+ removeQuietly(tried);
497
+ try {
498
+ fs.writeFileSync(tried, JSON.stringify({ at: Date.now(), reason: 'heal_started' }), { mode: 0o600, flag: 'wx' });
499
+ } catch {
500
+ removeQuietly(tried);
501
+ }
502
+ const result = ensure({ ...(options || {}), home, version: cfg.version, integrity: cfg.integrity, sdk: cfg.sdk, nodePath: process.execPath, npmExecPath: process.execPath, prune: false });
503
+ removeQuietly(mark);
504
+ if (result.state !== 'installed' && result.state !== 'present') fs.writeFileSync(mark, JSON.stringify({ at: Date.now(), reason: String(result.reason || 'failed') }), { mode: 0o600, flag: 'wx' });
505
+ else removeQuietly(tried);
506
+ return result;
507
+ } finally {
508
+ removeQuietly(lock);
509
+ }
510
+ }
511
+ function budgetFor(sub) {
512
+ if (sub === undefined) return 25000;
513
+ if (!PRE_RE.test(String(sub))) return 0;
514
+ return /^claude-/.test(sub) ? 45000 : /^cursor-/.test(sub) ? 12000 : 3500;
515
+ }
516
+ function sleep(ms) {
517
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
518
+ }
519
+ function awaitHeal(cfg, home, sub, loader) {
520
+ const held = { state: 'failed', outage: false };
521
+ const outage = (reason) => ({ state: 'failed', outage: OUTAGE.includes(reason) });
522
+ if (!VERSION_RE.test(cfg.version) || !INTEGRITY_RE.test(String(cfg.integrity || '')) || typeof loader !== 'string') return held;
523
+ const project = process.cwd();
524
+ const { lock, mark, tried } = healPaths(home, cfg.version, project);
525
+ for (const dir of [path.join(home, '.marrow'), path.dirname(rootOf(home)), rootOf(home)]) {
526
+ let stat = null;
527
+ try {
528
+ stat = fs.lstatSync(dir);
529
+ } catch {
530
+ stat = null;
531
+ }
532
+ const me = uid();
533
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink() || (me !== null && stat.uid !== me))) return held;
534
+ }
535
+ const started = Date.now();
536
+ let spawned = false;
537
+ const kick = () => {
538
+ if (lockBusy(lock) || Date.now() - failure(home, tried).at < ATTEMPT_MS) return false;
539
+ try {
540
+ const child = cp().spawn(process.execPath, ['-e', loader, '--', '--marrow-heal', '--package=@getmarrow/mcp@' + cfg.version, 'marrow-mcp'], { cwd: home, env: { ...process.env, PWD: project }, detached: true, stdio: 'ignore', windowsHide: true });
541
+ child.on('error', () => {});
542
+ child.unref();
543
+ } catch {
544
+ return null;
545
+ }
546
+ spawned = true;
547
+ return true;
548
+ };
549
+ const last = failure(home, mark);
550
+ if (started - last.at < RETRY_MS && !lockBusy(lock)) {
551
+ if (OUTAGE.includes(last.reason)) kick();
552
+ return outage(last.reason);
553
+ }
554
+ if (!lockBusy(lock) && kick() === null) return held;
555
+ const budget = budgetFor(sub);
556
+ while (Date.now() - started < budget) {
557
+ sleep(100);
558
+ const runtime = verify(home, cfg.version, cfg.integrity);
559
+ if (runtime) return runtime;
560
+ const last = failure(home, mark);
561
+ if (last.at >= started) return outage(last.reason);
562
+ if (!spawned && !lockBusy(lock)) {
563
+ const kicked = kick();
564
+ if (kicked === null) return held;
565
+ if (!kicked) return outage('heal_timeout');
566
+ }
567
+ }
568
+ return outage('heal_timeout');
569
+ }
570
+ function main(cfg, loader) {
571
+ const args = process.argv.slice(1);
572
+ const pre = args.some((arg) => PRE_RE.test(String(arg)));
573
+ const spec = '--package=@getmarrow/mcp@' + cfg.version;
574
+ const home = process.platform !== 'win32' && process.env.HOME ? process.env.HOME : require('node:os').homedir();
575
+ if (args[0] === '--marrow-heal') {
576
+ const result = args.length === 3 && args[1] === spec && args[2] === 'marrow-mcp' ? heal(cfg, home) : { state: 'skipped' };
577
+ process.exitCode = result.state === 'installed' || result.state === 'present' ? 0 : 1;
578
+ return;
579
+ }
580
+ let runtime = args[0] === spec && args[1] === 'marrow-mcp' ? verify(home, cfg.version, cfg.integrity) : null;
581
+ if (!runtime && args[0] === spec && args[1] === 'marrow-mcp') runtime = awaitHeal(cfg, home, args[2], loader);
582
+ if (!runtime || runtime.state !== 'verified') {
583
+ const warn = pre && runtime && runtime.outage === true;
584
+ process.stderr.write((warn ? WARN : pre ? HOLD : UNAVAILABLE) + '\n');
585
+ process.exitCode = pre && !warn ? 2 : 1;
586
+ return;
587
+ }
588
+ process.argv = [process.argv[0], runtime.bin, ...args.slice(2)];
589
+ require(runtime.bin);
590
+ }
591
+ return { SCHEMA, HOLD, UNAVAILABLE, WARN, OUTAGE, NPM_TIMEOUT_MS, LOCK_STALE_MS, RETRY_MS, ATTEMPT_MS, NETWORK_KEYS, configOf, verify, ensure, heal, npmInvocation, npmNetwork, npmOutside, npmForeign, ownerUrl, npmConfigMoves, npmOwnerRegistries, npmEnvironment, npmInstall, removeOtherRuntimes, budgetFor, healPaths, projectKey, main };
592
+ }
593
+
594
+ module.exports = { runtimeCore };