@getmarrow/mcp 3.9.102 → 3.9.103

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.
Files changed (109) hide show
  1. package/README.md +49 -23
  2. package/dist/cli.js +41 -10
  3. package/dist/cli.js.map +1 -1
  4. package/dist/codex-plugin-pins.d.ts +19 -0
  5. package/dist/codex-plugin-pins.d.ts.map +1 -0
  6. package/dist/codex-plugin-pins.js +43 -0
  7. package/dist/codex-plugin-pins.js.map +1 -0
  8. package/dist/codex-plugins.d.ts +34 -0
  9. package/dist/codex-plugins.d.ts.map +1 -0
  10. package/dist/codex-plugins.js +240 -0
  11. package/dist/codex-plugins.js.map +1 -0
  12. package/dist/hook-contract.d.ts +2 -2
  13. package/dist/hook-contract.d.ts.map +1 -1
  14. package/dist/hook-contract.js +8 -3
  15. package/dist/hook-contract.js.map +1 -1
  16. package/dist/hook-pre-action.d.ts +48 -2
  17. package/dist/hook-pre-action.d.ts.map +1 -1
  18. package/dist/hook-pre-action.js +195 -22
  19. package/dist/hook-pre-action.js.map +1 -1
  20. package/dist/hook.d.ts +1 -1
  21. package/dist/hook.d.ts.map +1 -1
  22. package/dist/hook.js +12 -6
  23. package/dist/hook.js.map +1 -1
  24. package/dist/host-session.d.ts +11 -0
  25. package/dist/host-session.d.ts.map +1 -1
  26. package/dist/host-session.js +49 -0
  27. package/dist/host-session.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +15 -10
  30. package/dist/index.js.map +1 -1
  31. package/dist/normalized-action.d.ts +29 -6
  32. package/dist/normalized-action.d.ts.map +1 -1
  33. package/dist/normalized-action.js +492 -102
  34. package/dist/normalized-action.js.map +1 -1
  35. package/dist/own-server-identity.d.ts +123 -0
  36. package/dist/own-server-identity.d.ts.map +1 -0
  37. package/dist/own-server-identity.js +1052 -0
  38. package/dist/own-server-identity.js.map +1 -0
  39. package/dist/own-tool.d.ts +7 -3
  40. package/dist/own-tool.d.ts.map +1 -1
  41. package/dist/own-tool.js +7 -3
  42. package/dist/own-tool.js.map +1 -1
  43. package/dist/redact.d.ts +23 -0
  44. package/dist/redact.d.ts.map +1 -1
  45. package/dist/redact.js +181 -8
  46. package/dist/redact.js.map +1 -1
  47. package/dist/seatbelt/classifier-pin.d.ts +15 -0
  48. package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
  49. package/dist/seatbelt/classifier-pin.js +26 -0
  50. package/dist/seatbelt/classifier-pin.js.map +1 -0
  51. package/dist/seatbelt/guards.d.ts +10 -0
  52. package/dist/seatbelt/guards.d.ts.map +1 -0
  53. package/dist/seatbelt/guards.js +509 -0
  54. package/dist/seatbelt/guards.js.map +1 -0
  55. package/dist/seatbelt/hook.d.ts +72 -0
  56. package/dist/seatbelt/hook.d.ts.map +1 -0
  57. package/dist/seatbelt/hook.js +362 -0
  58. package/dist/seatbelt/hook.js.map +1 -0
  59. package/dist/seatbelt/longopts.d.ts +68 -0
  60. package/dist/seatbelt/longopts.d.ts.map +1 -0
  61. package/dist/seatbelt/longopts.js +377 -0
  62. package/dist/seatbelt/longopts.js.map +1 -0
  63. package/dist/seatbelt/mapper.d.ts +83 -0
  64. package/dist/seatbelt/mapper.d.ts.map +1 -0
  65. package/dist/seatbelt/mapper.js +10248 -0
  66. package/dist/seatbelt/mapper.js.map +1 -0
  67. package/dist/seatbelt/record.d.ts +94 -0
  68. package/dist/seatbelt/record.d.ts.map +1 -0
  69. package/dist/seatbelt/record.js +776 -0
  70. package/dist/seatbelt/record.js.map +1 -0
  71. package/dist/seatbelt/tables.d.ts +82 -0
  72. package/dist/seatbelt/tables.d.ts.map +1 -0
  73. package/dist/seatbelt/tables.js +359 -0
  74. package/dist/seatbelt/tables.js.map +1 -0
  75. package/dist/seatbelt-deploy.d.ts +100 -0
  76. package/dist/seatbelt-deploy.d.ts.map +1 -0
  77. package/dist/seatbelt-deploy.js +393 -0
  78. package/dist/seatbelt-deploy.js.map +1 -0
  79. package/dist/seatbelt-fs.d.ts +30 -0
  80. package/dist/seatbelt-fs.d.ts.map +1 -0
  81. package/dist/seatbelt-fs.js +94 -0
  82. package/dist/seatbelt-fs.js.map +1 -0
  83. package/dist/seatbelt-git.d.ts +212 -0
  84. package/dist/seatbelt-git.d.ts.map +1 -0
  85. package/dist/seatbelt-git.js +844 -0
  86. package/dist/seatbelt-git.js.map +1 -0
  87. package/dist/seatbelt-mode.d.ts +77 -0
  88. package/dist/seatbelt-mode.d.ts.map +1 -0
  89. package/dist/seatbelt-mode.js +496 -0
  90. package/dist/seatbelt-mode.js.map +1 -0
  91. package/dist/seatbelt-path.d.ts +123 -0
  92. package/dist/seatbelt-path.d.ts.map +1 -0
  93. package/dist/seatbelt-path.js +482 -0
  94. package/dist/seatbelt-path.js.map +1 -0
  95. package/dist/seatbelt-scripts.d.ts +109 -0
  96. package/dist/seatbelt-scripts.d.ts.map +1 -0
  97. package/dist/seatbelt-scripts.js +1060 -0
  98. package/dist/seatbelt-scripts.js.map +1 -0
  99. package/dist/tool-catalog.js +2 -2
  100. package/dist/tool-catalog.js.map +1 -1
  101. package/dist/types.d.ts +2 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/package.json +2 -1
  104. package/server.json +3 -3
  105. package/vendor/classifier/MANIFEST.json +25 -0
  106. package/vendor/classifier/classifier.js +1356 -0
  107. package/vendor/classifier/policy-baseline.js +187 -0
  108. package/vendor/classifier/redact.js +59 -0
  109. package/vendor/classifier/shell-parse.js +455 -0
@@ -0,0 +1,1052 @@
1
+ "use strict";
2
+ /**
3
+ * Whether the `marrow` MCP server a Codex or Cursor hook label names is the
4
+ * official Marrow MCP server. Codex builds its label from the user's server
5
+ * name (servers `mcp__marrow`, `marrow-` and `marrow_` all give
6
+ * `mcp__marrow__<tool>` on codex 0.161), and Cursor's preToolUse label has no
7
+ * server at all, so the label alone never proves the server. The hook reads
8
+ * the host's own MCP configuration and skips the gate only when exactly one
9
+ * configured server can produce the label and it starts the official package.
10
+ *
11
+ * Fail closed: a missing, unreadable, oversized, non-regular or unparsable
12
+ * config, more than one colliding server, an unknown command, a wrapper, an
13
+ * env override, an enabled Codex plugin or a config override on the codex
14
+ * command line means "not proven": the call goes down the normal gate path.
15
+ * Reads are local, size-capped and done at most once per hook process; no
16
+ * config content is ever logged.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.MAX_HOST_CONFIG_BYTES = void 0;
20
+ exports.readHostConfigFile = readHostConfigFile;
21
+ exports.readHostConfigBytes = readHostConfigBytes;
22
+ exports.parseToml = parseToml;
23
+ exports.isOfficialMarrowServerLaunch = isOfficialMarrowServerLaunch;
24
+ exports.serverNameCollidesWithMarrow = serverNameCollidesWithMarrow;
25
+ exports.codexServerNameCanProduceMarrowLabel = codexServerNameCanProduceMarrowLabel;
26
+ exports.npmProjectConfigIsHarmless = npmProjectConfigIsHarmless;
27
+ exports.codexOfficialMarrowServer = codexOfficialMarrowServer;
28
+ exports.cursorOfficialMarrowServer = cursorOfficialMarrowServer;
29
+ exports.resetOwnServerIdentityCache = resetOwnServerIdentityCache;
30
+ exports.isOfficialMarrowMcpEventOnHost = isOfficialMarrowMcpEventOnHost;
31
+ const node_crypto_1 = require("node:crypto");
32
+ const node_fs_1 = require("node:fs");
33
+ const node_os_1 = require("node:os");
34
+ const node_path_1 = require("node:path");
35
+ const node_zlib_1 = require("node:zlib");
36
+ const host_session_1 = require("./host-session");
37
+ const codex_plugins_1 = require("./codex-plugins");
38
+ const own_tool_1 = require("./own-tool");
39
+ exports.MAX_HOST_CONFIG_BYTES = 256 * 1024;
40
+ const MAX_DIR_DEPTH = 64;
41
+ const MAX_ROOTS = 8;
42
+ const MAX_PATH_LENGTH = 4096;
43
+ /** One config file: missing adds nothing; anything but a readable regular file within the cap is bad. */
44
+ function readHostConfigFile(path) {
45
+ const read = readHostConfigBytes(path);
46
+ return read.state === 'ok' ? { state: 'ok', text: read.bytes.toString('utf8') } : read;
47
+ }
48
+ /** The same bounded read, as raw bytes (for content pins); `maxBytes` is the size cap. */
49
+ function readHostConfigBytes(path, maxBytes = exports.MAX_HOST_CONFIG_BYTES) {
50
+ let fd;
51
+ try {
52
+ fd = (0, node_fs_1.openSync)(path, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NONBLOCK ?? 0));
53
+ }
54
+ catch (error) {
55
+ const code = error?.code;
56
+ return code === 'ENOENT' || code === 'ENOTDIR' ? { state: 'missing' } : { state: 'bad', reason: `unreadable (${code || 'error'})` };
57
+ }
58
+ try {
59
+ const stat = (0, node_fs_1.fstatSync)(fd);
60
+ if (!stat.isFile())
61
+ return { state: 'bad', reason: 'not a regular file' };
62
+ if (stat.size > maxBytes)
63
+ return { state: 'bad', reason: 'over the size cap' };
64
+ const buffer = Buffer.alloc(maxBytes + 1);
65
+ let total = 0;
66
+ for (;;) {
67
+ const read = (0, node_fs_1.readSync)(fd, buffer, total, buffer.length - total, null);
68
+ if (read <= 0)
69
+ break;
70
+ total += read;
71
+ if (total > maxBytes)
72
+ return { state: 'bad', reason: 'over the size cap' };
73
+ }
74
+ return { state: 'ok', bytes: Buffer.from(buffer.subarray(0, total)) };
75
+ }
76
+ catch {
77
+ return { state: 'bad', reason: 'unreadable' };
78
+ }
79
+ finally {
80
+ try {
81
+ (0, node_fs_1.closeSync)(fd);
82
+ }
83
+ catch { /* closed */ }
84
+ }
85
+ }
86
+ /**
87
+ * A real git repository root: `.git` is a directory (not a symlink) holding a
88
+ * regular `HEAD` file and `objects` and `refs` directories, as `git init` makes.
89
+ * codex does not stop at a `.git` that is not a repository (an empty folder,
90
+ * an archive, a generated directory), so neither does this walk. A gitfile
91
+ * (`.git` file of a worktree or submodule) never stops it either: walking on
92
+ * reads more configs than the host, which only adds to the union (stricter).
93
+ */
94
+ function realGitRoot(dir) {
95
+ try {
96
+ const git = (0, node_fs_1.lstatSync)((0, node_path_1.join)(dir, '.git'));
97
+ if (!git.isDirectory())
98
+ return false;
99
+ return (0, node_fs_1.lstatSync)((0, node_path_1.join)(dir, '.git', 'HEAD')).isFile()
100
+ && (0, node_fs_1.lstatSync)((0, node_path_1.join)(dir, '.git', 'objects')).isDirectory()
101
+ && (0, node_fs_1.lstatSync)((0, node_path_1.join)(dir, '.git', 'refs')).isDirectory();
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ /**
108
+ * Every `<dir>/<rel>` from each start directory up to its project root (the
109
+ * nearest real git repository root, where codex stops looking), or up to the
110
+ * filesystem root when there is none; deduplicated. A path deeper than the
111
+ * bound proves nothing (null).
112
+ */
113
+ function upwardPaths(starts, rel) {
114
+ const out = new Set();
115
+ for (const start of starts) {
116
+ let dir = (0, node_path_1.resolve)(start);
117
+ for (let depth = 0;; depth += 1) {
118
+ if (depth >= MAX_DIR_DEPTH)
119
+ return null;
120
+ out.add((0, node_path_1.join)(dir, rel));
121
+ if (realGitRoot(dir))
122
+ break;
123
+ const parent = (0, node_path_1.dirname)(dir);
124
+ if (parent === dir)
125
+ break;
126
+ dir = parent;
127
+ }
128
+ }
129
+ return [...out];
130
+ }
131
+ /** One leading U+FEFF (Windows editors write it; codex 0.161 accepts it) is not content. */
132
+ function withoutBom(text) {
133
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
134
+ }
135
+ function usableDir(value) {
136
+ return typeof value === 'string' && value.length > 0 && value.length <= MAX_PATH_LENGTH && (0, node_path_1.isAbsolute)(value) && !value.includes('\0') ? value : null;
137
+ }
138
+ const newTable = () => Object.create(null);
139
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)?|^\d{2}:\d{2}:\d{2}(?:\.\d+)?/;
140
+ function parseToml(text) {
141
+ if (text.includes('\0'))
142
+ throw new Error('NUL in config');
143
+ const root = newTable();
144
+ const explicit = new Set();
145
+ const frozen = new Set();
146
+ let pos = 0;
147
+ let current = root;
148
+ const fail = (what) => { throw new Error(`TOML: ${what} at ${pos}`); };
149
+ const skipWs = () => { while (pos < text.length && (text[pos] === ' ' || text[pos] === '\t'))
150
+ pos += 1; };
151
+ const skipComment = () => {
152
+ if (text[pos] === '#') {
153
+ while (pos < text.length && text[pos] !== '\n')
154
+ pos += 1;
155
+ }
156
+ };
157
+ const skipBlank = () => {
158
+ for (;;) {
159
+ skipWs();
160
+ skipComment();
161
+ if (text[pos] === '\r' && text[pos + 1] === '\n') {
162
+ pos += 2;
163
+ continue;
164
+ }
165
+ if (text[pos] === '\n') {
166
+ pos += 1;
167
+ continue;
168
+ }
169
+ return;
170
+ }
171
+ };
172
+ const endOfLine = () => {
173
+ skipWs();
174
+ skipComment();
175
+ if (pos >= text.length)
176
+ return;
177
+ if (text[pos] === '\r' && text[pos + 1] === '\n') {
178
+ pos += 2;
179
+ return;
180
+ }
181
+ if (text[pos] === '\n') {
182
+ pos += 1;
183
+ return;
184
+ }
185
+ fail('expected end of line');
186
+ };
187
+ const escape = () => {
188
+ const c = text[pos];
189
+ pos += 1;
190
+ const simple = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
191
+ if (c in simple)
192
+ return simple[c];
193
+ if (c === 'u' || c === 'U') {
194
+ const len = c === 'u' ? 4 : 8;
195
+ const hex = text.slice(pos, pos + len);
196
+ if (!/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== len)
197
+ fail('bad unicode escape');
198
+ pos += len;
199
+ const code = parseInt(hex, 16);
200
+ if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff))
201
+ fail('bad code point');
202
+ return String.fromCodePoint(code);
203
+ }
204
+ return fail('bad escape');
205
+ };
206
+ const basicString = () => {
207
+ if (text.startsWith('"""', pos)) {
208
+ pos += 3;
209
+ if (text[pos] === '\n')
210
+ pos += 1;
211
+ else if (text.startsWith('\r\n', pos))
212
+ pos += 2;
213
+ let out = '';
214
+ for (;;) {
215
+ if (pos >= text.length)
216
+ fail('unterminated string');
217
+ if (text.startsWith('"""', pos)) {
218
+ let quotes = 3;
219
+ while (text[pos + quotes] === '"' && quotes < 5)
220
+ quotes += 1;
221
+ out += '"'.repeat(quotes - 3);
222
+ pos += quotes;
223
+ return out;
224
+ }
225
+ const c = text[pos];
226
+ if (c === '\\') {
227
+ pos += 1;
228
+ if (/^[ \t]*\r?\n/.test(text.slice(pos, pos + 64)) || text[pos] === '\n' || text[pos] === '\r') {
229
+ while (pos < text.length && /[ \t\r\n]/.test(text[pos]))
230
+ pos += 1;
231
+ }
232
+ else
233
+ out += escape();
234
+ continue;
235
+ }
236
+ out += c;
237
+ pos += 1;
238
+ }
239
+ }
240
+ pos += 1;
241
+ let out = '';
242
+ for (;;) {
243
+ if (pos >= text.length || text[pos] === '\n')
244
+ fail('unterminated string');
245
+ const c = text[pos];
246
+ if (c === '"') {
247
+ pos += 1;
248
+ return out;
249
+ }
250
+ if (c === '\\') {
251
+ pos += 1;
252
+ out += escape();
253
+ continue;
254
+ }
255
+ out += c;
256
+ pos += 1;
257
+ }
258
+ };
259
+ const literalString = () => {
260
+ if (text.startsWith("'''", pos)) {
261
+ pos += 3;
262
+ if (text[pos] === '\n')
263
+ pos += 1;
264
+ else if (text.startsWith('\r\n', pos))
265
+ pos += 2;
266
+ const end = text.indexOf("'''", pos);
267
+ if (end < 0)
268
+ fail('unterminated string');
269
+ let stop = end + 3;
270
+ while (text[stop] === "'" && stop - end < 5)
271
+ stop += 1;
272
+ const out = text.slice(pos, stop - 3);
273
+ pos = stop;
274
+ return out;
275
+ }
276
+ pos += 1;
277
+ const end = text.indexOf("'", pos);
278
+ const nl = text.indexOf('\n', pos);
279
+ if (end < 0 || (nl >= 0 && nl < end))
280
+ fail('unterminated string');
281
+ const out = text.slice(pos, end);
282
+ pos = end + 1;
283
+ return out;
284
+ };
285
+ const keyPart = () => {
286
+ skipWs();
287
+ if (text[pos] === '"') {
288
+ if (text.startsWith('"""', pos))
289
+ fail('multi-line key');
290
+ return basicString();
291
+ }
292
+ if (text[pos] === "'") {
293
+ if (text.startsWith("'''", pos))
294
+ fail('multi-line key');
295
+ return literalString();
296
+ }
297
+ const match = /^[A-Za-z0-9_-]+/.exec(text.slice(pos, pos + 256));
298
+ if (!match)
299
+ fail('bad key');
300
+ pos += match[0].length;
301
+ return match[0];
302
+ };
303
+ const dottedKey = () => {
304
+ const parts = [keyPart()];
305
+ for (;;) {
306
+ skipWs();
307
+ if (text[pos] !== '.')
308
+ return parts;
309
+ pos += 1;
310
+ parts.push(keyPart());
311
+ }
312
+ };
313
+ const has = (table, key) => Object.prototype.hasOwnProperty.call(table, key);
314
+ const descend = (table, key, forHeader) => {
315
+ if (!has(table, key)) {
316
+ const next = newTable();
317
+ table[key] = next;
318
+ return next;
319
+ }
320
+ const value = table[key];
321
+ if (Array.isArray(value) && forHeader && !frozen.has(value) && value.length > 0) {
322
+ const last = value[value.length - 1];
323
+ if (last && typeof last === 'object' && !Array.isArray(last))
324
+ return last;
325
+ }
326
+ if (!value || typeof value !== 'object' || Array.isArray(value) || frozen.has(value))
327
+ return fail('key redefined');
328
+ return value;
329
+ };
330
+ const value = (depth) => {
331
+ if (depth > 32)
332
+ fail('nesting too deep');
333
+ skipWs();
334
+ const c = text[pos];
335
+ if (c === '"')
336
+ return basicString();
337
+ if (c === "'")
338
+ return literalString();
339
+ if (c === '[') {
340
+ pos += 1;
341
+ const items = [];
342
+ for (;;) {
343
+ skipBlank();
344
+ if (text[pos] === ']') {
345
+ pos += 1;
346
+ break;
347
+ }
348
+ items.push(value(depth + 1));
349
+ skipBlank();
350
+ if (text[pos] === ',') {
351
+ pos += 1;
352
+ continue;
353
+ }
354
+ if (text[pos] === ']') {
355
+ pos += 1;
356
+ break;
357
+ }
358
+ fail('bad array');
359
+ }
360
+ frozen.add(items);
361
+ return items;
362
+ }
363
+ if (c === '{') {
364
+ pos += 1;
365
+ const table = newTable();
366
+ skipWs();
367
+ if (text[pos] === '}') {
368
+ pos += 1;
369
+ frozen.add(table);
370
+ return table;
371
+ }
372
+ for (;;) {
373
+ const key = dottedKey();
374
+ skipWs();
375
+ if (text[pos] !== '=')
376
+ fail('expected =');
377
+ pos += 1;
378
+ assign(table, key, value(depth + 1));
379
+ skipWs();
380
+ if (text[pos] === ',') {
381
+ pos += 1;
382
+ continue;
383
+ }
384
+ if (text[pos] === '}') {
385
+ pos += 1;
386
+ break;
387
+ }
388
+ fail('bad inline table');
389
+ }
390
+ frozen.add(table);
391
+ return table;
392
+ }
393
+ const rest = text.slice(pos, pos + 64);
394
+ const date = DATE_TIME.exec(rest);
395
+ if (date) {
396
+ pos += date[0].length;
397
+ return date[0];
398
+ }
399
+ const word = /^(?:true|false|[+-]?(?:inf|nan)|0x[0-9A-Fa-f_]+|0o[0-7_]+|0b[01_]+|[+-]?\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?)/.exec(rest);
400
+ if (!word)
401
+ return fail('bad value');
402
+ pos += word[0].length;
403
+ if (word[0] === 'true')
404
+ return true;
405
+ if (word[0] === 'false')
406
+ return false;
407
+ return Number(word[0].replace(/_/g, ''));
408
+ };
409
+ const assign = (table, key, val) => {
410
+ let target = table;
411
+ for (const part of key.slice(0, -1))
412
+ target = descend(target, part, false);
413
+ const last = key[key.length - 1];
414
+ if (has(target, last))
415
+ fail('duplicate key');
416
+ target[last] = val;
417
+ };
418
+ for (;;) {
419
+ skipBlank();
420
+ if (pos >= text.length)
421
+ break;
422
+ if (text[pos] === '[') {
423
+ const array = text[pos + 1] === '[';
424
+ pos += array ? 2 : 1;
425
+ const key = dottedKey();
426
+ skipWs();
427
+ if (!text.startsWith(array ? ']]' : ']', pos))
428
+ fail('bad table header');
429
+ pos += array ? 2 : 1;
430
+ endOfLine();
431
+ let target = root;
432
+ for (const part of key.slice(0, -1))
433
+ target = descend(target, part, true);
434
+ const last = key[key.length - 1];
435
+ if (array) {
436
+ if (!has(target, last))
437
+ target[last] = [];
438
+ const list = target[last];
439
+ if (!Array.isArray(list) || frozen.has(list))
440
+ fail('key redefined');
441
+ const next = newTable();
442
+ list.push(next);
443
+ current = next;
444
+ }
445
+ else {
446
+ const existing = has(target, last) ? target[last] : undefined;
447
+ if (existing !== undefined) {
448
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing) || frozen.has(existing) || explicit.has(existing))
449
+ fail('table redefined');
450
+ current = existing;
451
+ }
452
+ else {
453
+ current = newTable();
454
+ target[last] = current;
455
+ }
456
+ explicit.add(current);
457
+ }
458
+ continue;
459
+ }
460
+ const key = dottedKey();
461
+ skipWs();
462
+ if (text[pos] !== '=')
463
+ fail('expected =');
464
+ pos += 1;
465
+ assign(current, key, value(0));
466
+ endOfLine();
467
+ }
468
+ return root;
469
+ }
470
+ // ---------------------------------------------------------------------------
471
+ // The official server launch
472
+ // ---------------------------------------------------------------------------
473
+ const PACKAGE_SPEC = /^@getmarrow\/mcp(?:@(?:latest|\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[0-9A-Za-z.]{1,32})?))?$/;
474
+ const PINNED_SPEC = /^--package=@getmarrow\/mcp@(\d{1,4}\.\d{1,4}\.\d{1,4})$/;
475
+ // The installer's runtime core (src/runtime-core.js, identical in @getmarrow/install 0.1.70
476
+ // 60535d61 and feat/install-train-2 03a85d36): SHA-256 of the trimmed source its loader embeds.
477
+ // A new installer core must be added here before its local-runtime server counts as official.
478
+ const INSTALLER_RUNTIME_CORE_SHA256 = new Set(['73ea57796c017abfd144b43f3b91c9c935314c156ba523cbca525ebc1f977cb0']);
479
+ const INSTALLER_UNAVAILABLE_TEXT = 'Marrow could not start its verified local runtime. The owner can repair it by running npx -y @getmarrow/install@latest --repair --cwd <project folder> from a terminal outside the project.';
480
+ const LOADER_RE = /^eval\(global\.MARROW_L=require\(\/node:zlib\/\.source\)\.brotliDecompressSync\(Buffer\.from\(\/([A-Za-z0-9_-]{16,16000})\/\.source,\/base64url\/\.source\)\)\+\[\]\)$/;
481
+ const LOADER_HEAD = "'use strict';\nconst CFG = ";
482
+ const LOADER_TAIL = ')().main(CFG, global.MARROW_L);\n';
483
+ /** The installer's POSIX server script for one version (its mcpServerLaunch), byte for byte. */
484
+ function installerServerScript(version) {
485
+ return `N="$HOME/.marrow/runtime/mcp/${version}/node"; if [ ! -x "$N" ]; then N=; P="$PATH:/usr/local/bin:/opt/homebrew/bin"; while [ -n "$P" ]; do D="\${P%%:*}"; case "$P" in (*:*) P="\${P#*:}" ;; (*) P= ;; esac; case "$D" in ("$PWD"|"$PWD"/*) ;; (/*) if [ -f "$D/node" ] && [ -x "$D/node" ]; then N="$D/node"; break; fi ;; esac; done; fi; if [ -n "$N" ]; then exec "$N" -e "$0" -- "$@"; fi; printf "%s\\n" "${INSTALLER_UNAVAILABLE_TEXT}" >&2; exit 1`;
486
+ }
487
+ /** Whether a loader decodes to exactly the installer's launcher for this version, with a pinned core. */
488
+ function installerLoaderFor(loader, version) {
489
+ const match = LOADER_RE.exec(loader);
490
+ if (!match)
491
+ return false;
492
+ let source;
493
+ try {
494
+ source = (0, node_zlib_1.brotliDecompressSync)(Buffer.from(match[1], 'base64url'), { maxOutputLength: 1 << 17 }).toString('utf8');
495
+ }
496
+ catch {
497
+ return false;
498
+ }
499
+ if (!source.startsWith(LOADER_HEAD))
500
+ return false;
501
+ const lineEnd = source.indexOf('\n', LOADER_HEAD.length);
502
+ if (lineEnd < 0)
503
+ return false;
504
+ const cfgLine = source.slice(LOADER_HEAD.length, lineEnd);
505
+ if (!cfgLine.endsWith(';'))
506
+ return false;
507
+ let cfg;
508
+ try {
509
+ cfg = JSON.parse(cfgLine.slice(0, -1));
510
+ }
511
+ catch {
512
+ return false;
513
+ }
514
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg) || JSON.stringify(cfg) !== cfgLine.slice(0, -1))
515
+ return false;
516
+ if (cfg.version !== version)
517
+ return false;
518
+ const body = source.slice(lineEnd + 1);
519
+ if (!body.startsWith('(') || !body.endsWith(LOADER_TAIL))
520
+ return false;
521
+ const core = body.slice(1, -LOADER_TAIL.length);
522
+ return INSTALLER_RUNTIME_CORE_SHA256.has((0, node_crypto_1.createHash)('sha256').update(core).digest('hex'));
523
+ }
524
+ /**
525
+ * Whether (command, args) start the official Marrow MCP server, in one of the
526
+ * exact forms Marrow documents or writes:
527
+ * npx [-y|--yes] @getmarrow/mcp[@latest|@X.Y.Z]
528
+ * npx [-y|--yes] --package=@getmarrow/mcp[@latest|@X.Y.Z] marrow-mcp
529
+ * marrow-mcp (the installed bin, no args)
530
+ * (npx.cmd and marrow-mcp.cmd only on Windows)
531
+ * /bin/sh -c <installer script> <installer loader> --package=@getmarrow/mcp@V marrow-mcp
532
+ * node -e <installer loader> -- --package=@getmarrow/mcp@V marrow-mcp
533
+ * Nothing appended, no wrapper, no other package.
534
+ */
535
+ function isOfficialMarrowServerLaunch(command, args) {
536
+ if (typeof command !== 'string')
537
+ return false;
538
+ const list = args === undefined ? [] : args;
539
+ if (!Array.isArray(list) || list.length > 8 || !list.every((arg) => typeof arg === 'string'))
540
+ return false;
541
+ const argv = list;
542
+ // The `.cmd` shims exist only on Windows; no Marrow installer or README writes them elsewhere.
543
+ const windows = process.platform === 'win32';
544
+ if (command === 'npx' || (windows && command === 'npx.cmd')) {
545
+ const rest = argv[0] === '-y' || argv[0] === '--yes' ? argv.slice(1) : argv;
546
+ if (rest.length === 1)
547
+ return PACKAGE_SPEC.test(rest[0]);
548
+ return rest.length === 2 && rest[0].startsWith('--package=') && PACKAGE_SPEC.test(rest[0].slice('--package='.length)) && rest[1] === 'marrow-mcp';
549
+ }
550
+ if (command === 'marrow-mcp' || (windows && command === 'marrow-mcp.cmd'))
551
+ return argv.length === 0;
552
+ if (command === '/bin/sh') {
553
+ const pinned = argv.length === 5 ? PINNED_SPEC.exec(argv[3]) : null;
554
+ return Boolean(pinned) && argv[0] === '-c' && argv[4] === 'marrow-mcp'
555
+ && argv[1] === installerServerScript(pinned[1]) && installerLoaderFor(argv[2], pinned[1]);
556
+ }
557
+ if (command === 'node') {
558
+ const pinned = argv.length === 5 ? PINNED_SPEC.exec(argv[3]) : null;
559
+ return Boolean(pinned) && argv[0] === '-e' && argv[2] === '--' && argv[4] === 'marrow-mcp' && installerLoaderFor(argv[1], pinned[1]);
560
+ }
561
+ return false;
562
+ }
563
+ /** A server name a host could turn into the `marrow` label: case, separators and leading `mcp` ignored (wider than any host). */
564
+ function serverNameCollidesWithMarrow(name) {
565
+ let folded = name.normalize('NFKC').toLowerCase().replace(/[^a-z0-9]/g, '');
566
+ while (folded.startsWith('mcp') && folded !== 'mcp')
567
+ folded = folded.slice(3);
568
+ return folded === 'marrow';
569
+ }
570
+ const CODEX_PLAIN_SERVER_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
571
+ /**
572
+ * Codex joins namespace and tool name with `__`, so a server whose namespace
573
+ * trims to `mcp` (`_`, `-`, `__`, `mcp__`, `mcp___`, `MCP__`, and the hashed
574
+ * duplicates of those) plus a tool `marrow__<own>` gives the exact label
575
+ * `mcp__marrow__<own>`. On Codex such a name, or any name outside the plain
576
+ * ASCII form, counts as able to produce the marrow label.
577
+ */
578
+ function codexServerNameCanProduceMarrowLabel(name) {
579
+ if (!CODEX_PLAIN_SERVER_NAME.test(name))
580
+ return true;
581
+ const folded = name.normalize('NFKC').toLowerCase().replace(/[^a-z0-9]/g, '');
582
+ return folded === '' || folded === 'mcp' || serverNameCollidesWithMarrow(name);
583
+ }
584
+ const SERVER_ENV_KEYS = new Set(['MARROW_BASE_URL', 'MARROW_FLEET_AGENT_ID', 'MARROW_TOOL_PROFILE', 'MARROW_API_KEY']);
585
+ const CODEX_SERVER_KEYS = new Set(['command', 'args', 'env', 'enabled', 'required', 'startup_timeout_sec', 'startup_timeout_ms', 'tool_timeout_sec', 'enabled_tools', 'disabled_tools']);
586
+ const CURSOR_SERVER_KEYS = new Set(['command', 'args', 'env', 'type']);
587
+ /**
588
+ * npx and a bare bin name resolve a project-local copy first: a
589
+ * `node_modules/@getmarrow/mcp` or `node_modules/.bin/marrow-mcp` in any
590
+ * working directory or above it could be planted project content, so with
591
+ * one present those two forms prove nothing. npx also reads the project's
592
+ * `.npmrc` (registry, scoped registry), so a `.npmrc` in a working directory
593
+ * or above it, other than the user's own `$HOME/.npmrc`, unproves the npx
594
+ * forms unless it only sets keys that cannot change what npx gets or runs
595
+ * (`projectNpmrc`). A path deeper than the walk bound proves nothing.
596
+ */
597
+ function upwardHas(starts, present) {
598
+ const seen = new Set();
599
+ for (const start of starts) {
600
+ let dir = (0, node_path_1.resolve)(start);
601
+ for (let depth = 0; !seen.has(dir); depth += 1) {
602
+ if (depth >= MAX_DIR_DEPTH)
603
+ return true;
604
+ seen.add(dir);
605
+ if (present(dir))
606
+ return true;
607
+ const parent = (0, node_path_1.dirname)(dir);
608
+ if (parent === dir)
609
+ break;
610
+ dir = parent;
611
+ }
612
+ }
613
+ return false;
614
+ }
615
+ function localPackageCopy(starts) {
616
+ return upwardHas(starts, (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'node_modules', '@getmarrow', 'mcp'))
617
+ || ['marrow-mcp', 'marrow-mcp.cmd', 'npx', 'npx.cmd'].some((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'node_modules', '.bin', name))));
618
+ }
619
+ /** A project npm config larger than this proves nothing; real ones are a few lines. */
620
+ const MAX_NPM_PROJECT_CONFIG_BYTES = 64 * 1024;
621
+ /**
622
+ * The project npm config keys that cannot change what npx downloads, from
623
+ * where, with what trust, or what it runs: output and logging, `npm install`
624
+ * save and `npm version` / `npm init` settings, lockfile and peer-resolution
625
+ * switches within the same registry tree, and pnpm/yarn names npm ignores.
626
+ * Matching is exact and case-sensitive. Registry, scoped registry, proxy, TLS,
627
+ * cache, prefix, script, include/omit and location keys are deliberately not
628
+ * here, and neither is anything not listed.
629
+ */
630
+ const NPM_PROJECT_CONFIG_KEYS = new Set([
631
+ 'save-exact', 'save-prefix', 'fund', 'audit', 'audit-level', 'loglevel', 'progress', 'color', 'unicode', 'update-notifier',
632
+ 'package-lock', 'package-lock-only', 'lockfile-version', 'legacy-peer-deps', 'strict-peer-deps',
633
+ 'git-tag-version', 'sign-git-tag', 'tag-version-prefix', 'message',
634
+ 'init-author-name', 'init-author-email', 'init-license', 'init-version',
635
+ 'auto-install-peers', 'shamefully-hoist', 'strict-peer-dependencies', 'node-linker', 'link-workspace-packages', 'prefer-workspace-packages',
636
+ ]);
637
+ /** The default registry's own credential fields. The value is never read or logged. */
638
+ const NPM_DEFAULT_REGISTRY_AUTH_KEY = /^\/\/registry\.npmjs\.org\/:(?:_authToken|_auth|_password|username|email|always-auth)$/;
639
+ /** Control characters (NUL, VT, FF, DEL, …) and the Unicode line separators: text a line reader may split or truncate at. */
640
+ const NPM_CONFIG_ODD_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u0085\u{2028}\u{2029}]/u;
641
+ /** ASCII blanks only: a key padded with any other whitespace is not a key on the list. */
642
+ function trimBlanks(text) {
643
+ let start = 0;
644
+ let end = text.length;
645
+ while (start < end && (text[start] === ' ' || text[start] === '\t'))
646
+ start += 1;
647
+ while (end > start && (text[end - 1] === ' ' || text[end - 1] === '\t'))
648
+ end -= 1;
649
+ return text.slice(start, end);
650
+ }
651
+ /** A value the way npm's ini reader sees it: trimmed, then unquoted or cut at the first `;` or `#`. */
652
+ function npmIniValue(raw) {
653
+ const value = trimBlanks(raw);
654
+ const quote = value[0];
655
+ if ((quote === '"' || quote === "'") && value.length >= 2 && value[value.length - 1] === quote)
656
+ return value.slice(1, -1);
657
+ const comment = value.search(/[;#]/);
658
+ return trimBlanks(comment < 0 ? value : value.slice(0, comment));
659
+ }
660
+ /**
661
+ * Whether the text of a project npm config file only sets keys on the list
662
+ * above (or the default registry's credential fields), so it cannot change
663
+ * what `npx -y @getmarrow/mcp` gets or runs. It reads like npm's ini reader
664
+ * where that is exact, and answers false for anything it does not understand:
665
+ * a section header, a line without `=`, a line ending in a backslash (a
666
+ * continuation in other ini dialects), a quoted or bracketed key, a key with
667
+ * a comment mark, `${…}` or any non-ASCII character in it, a lone CR (npm
668
+ * splits lines there, other readers do not), a control character or Unicode
669
+ * line separator anywhere, a BOM, a key that is not listed
670
+ * (so `registry`, any `@scope:registry`, any other `//host/:…`, `cache`,
671
+ * `proxy`, `cafile`, `node-options` and every other key). Comments (`;`, `#`)
672
+ * and blank lines are ignored. The last `engine-strict` line wins and must be
673
+ * `true` or `false`.
674
+ */
675
+ function npmProjectConfigIsHarmless(text) {
676
+ if (NPM_CONFIG_ODD_CHARS.test(text))
677
+ return false;
678
+ const lines = text.split('\n');
679
+ let engineStrict = null;
680
+ for (let index = 0; index < lines.length; index += 1) {
681
+ let line = lines[index];
682
+ if (index < lines.length - 1 && line.endsWith('\r'))
683
+ line = line.slice(0, -1);
684
+ if (line.includes('\r'))
685
+ return false;
686
+ const trimmed = trimBlanks(line);
687
+ if (trimmed.endsWith('\\'))
688
+ return false;
689
+ if (trimmed === '' || trimmed[0] === ';' || trimmed[0] === '#')
690
+ continue;
691
+ const equals = trimmed.indexOf('=');
692
+ if (equals < 0)
693
+ return false;
694
+ const key = trimBlanks(trimmed.slice(0, equals));
695
+ if (key === 'engine-strict') {
696
+ engineStrict = npmIniValue(trimmed.slice(equals + 1));
697
+ continue;
698
+ }
699
+ if (!NPM_PROJECT_CONFIG_KEYS.has(key) && !NPM_DEFAULT_REGISTRY_AUTH_KEY.test(key))
700
+ return false;
701
+ }
702
+ return engineStrict === null || engineStrict === 'true' || engineStrict === 'false';
703
+ }
704
+ /** One project npm config file: absent, or a plain readable text file whose keys are all harmless. */
705
+ function npmProjectConfigIsInert(file) {
706
+ const read = readHostConfigBytes(file, MAX_NPM_PROJECT_CONFIG_BYTES);
707
+ if (read.state === 'missing')
708
+ return true;
709
+ if (read.state === 'bad')
710
+ return false;
711
+ const text = read.bytes.toString('utf8');
712
+ if (!Buffer.from(text, 'utf8').equals(read.bytes))
713
+ return false;
714
+ return npmProjectConfigIsHarmless(text);
715
+ }
716
+ function projectNpmrc(starts, home) {
717
+ const userConfig = (0, node_path_1.resolve)(home, '.npmrc');
718
+ return upwardHas(starts, (dir) => {
719
+ const file = (0, node_path_1.join)(dir, '.npmrc');
720
+ return file !== userConfig && !npmProjectConfigIsInert(file);
721
+ });
722
+ }
723
+ /** npm reads any `npm_config_*` variable (any case) as config: registry, userconfig, globalconfig. */
724
+ function npmConfigEnv(env) {
725
+ return Object.keys(env).some((key) => /^npm_config_/i.test(key));
726
+ }
727
+ /** Whether a launch resolves through npx or PATH (the forms a local copy can shadow). */
728
+ function resolvesLocally(entry) {
729
+ const command = entry && typeof entry === 'object' ? entry.command : undefined;
730
+ return command === 'npx' || command === 'npx.cmd' || command === 'marrow-mcp' || command === 'marrow-mcp.cmd';
731
+ }
732
+ /** Whether a launch goes through npx (the forms npm config can redirect). */
733
+ function launchesWithNpx(entry) {
734
+ const command = entry && typeof entry === 'object' ? entry.command : undefined;
735
+ return command === 'npx' || command === 'npx.cmd';
736
+ }
737
+ /** One server definition: an official launch with only the keys and env names Marrow writes. */
738
+ function officialDefinition(entry, keys) {
739
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
740
+ return false;
741
+ const record = entry;
742
+ const own = Object.keys(record);
743
+ if (!own.every((key) => keys.has(key)))
744
+ return false;
745
+ if (record.type !== undefined && record.type !== 'stdio')
746
+ return false;
747
+ if (record.env !== undefined) {
748
+ const env = record.env;
749
+ if (!env || typeof env !== 'object' || Array.isArray(env))
750
+ return false;
751
+ if (!Object.keys(env).every((key) => SERVER_ENV_KEYS.has(key) && typeof env[key] === 'string'))
752
+ return false;
753
+ }
754
+ return isOfficialMarrowServerLaunch(record.command, record.args);
755
+ }
756
+ function addServers(into, servers, where) {
757
+ if (servers === undefined)
758
+ return;
759
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
760
+ into.bad = `${where}: servers is not a table`;
761
+ return;
762
+ }
763
+ for (const name of Object.keys(servers)) {
764
+ const list = into.names.get(name) ?? [];
765
+ list.push(servers[name]);
766
+ into.names.set(name, list);
767
+ }
768
+ }
769
+ function decide(set, keys, onlyServer, cwds, npm, codexNames = false) {
770
+ if (set.bad)
771
+ return { official: false, reason: set.bad };
772
+ const colliding = [...set.names.keys()].filter(codexNames ? codexServerNameCanProduceMarrowLabel : serverNameCollidesWithMarrow);
773
+ if (colliding.length !== 1)
774
+ return { official: false, reason: colliding.length ? 'more than one server can produce the marrow label' : 'no marrow server configured' };
775
+ if (colliding[0] !== 'marrow')
776
+ return { official: false, reason: 'the colliding server is not named exactly marrow' };
777
+ if (!set.names.get('marrow').every((entry) => officialDefinition(entry, keys)))
778
+ return { official: false, reason: 'the marrow server is not the official Marrow MCP launch' };
779
+ if (set.names.get('marrow').some(resolvesLocally) && localPackageCopy(cwds))
780
+ return { official: false, reason: 'a project-local Marrow MCP copy could shadow the npx or bin launch' };
781
+ if (set.names.get('marrow').some(launchesWithNpx) && (npmConfigEnv(npm.env) || projectNpmrc(cwds, npm.home)))
782
+ return { official: false, reason: 'npm config in the project or the environment could redirect the npx launch' };
783
+ if (onlyServer && set.names.size !== 1)
784
+ return { official: false, reason: 'other MCP servers could expose a tool with this name' };
785
+ return { official: true, reason: 'official' };
786
+ }
787
+ const PROFILE_NAME = /^[A-Za-z0-9_-][A-Za-z0-9_.-]{0,63}$/;
788
+ function codexArgsFacts(args) {
789
+ if (!args)
790
+ return { bad: null, profile: null, dirs: [] };
791
+ if (args.length >= 64)
792
+ return { bad: 'codex command line may be cut', profile: null, dirs: [] };
793
+ let profile = null;
794
+ const dirs = [];
795
+ const tokens = args.slice(1);
796
+ for (let i = 0; i < tokens.length; i += 1) {
797
+ const token = tokens[i];
798
+ const valueOf = (long, short) => {
799
+ if (token === long || token === short) {
800
+ i += 1;
801
+ return i < tokens.length ? tokens[i] : '';
802
+ }
803
+ if (token.startsWith(`${long}=`))
804
+ return token.slice(long.length + 1);
805
+ if (token.startsWith(short) && !token.startsWith('--') && token.length > short.length)
806
+ return token.slice(short.length).replace(/^=/, '');
807
+ return null;
808
+ };
809
+ const config = valueOf('--config', '-c');
810
+ if (config !== null) {
811
+ if (/mcp_servers|plugins/i.test(config))
812
+ return { bad: 'a codex -c override touches MCP servers or plugins', profile: null, dirs: [] };
813
+ continue;
814
+ }
815
+ const prof = valueOf('--profile', '-p');
816
+ if (prof !== null) {
817
+ profile = prof;
818
+ continue;
819
+ }
820
+ const cd = valueOf('--cd', '-C');
821
+ if (cd !== null) {
822
+ const dir = usableDir(cd);
823
+ if (dir)
824
+ dirs.push(dir);
825
+ }
826
+ }
827
+ return { bad: null, profile, dirs };
828
+ }
829
+ /** Whether the Codex `marrow` server is provably the official Marrow MCP server. */
830
+ function codexOfficialMarrowServer(options = {}) {
831
+ const env = options.env ?? process.env;
832
+ const read = options.read ?? readHostConfigFile;
833
+ const facts = codexArgsFacts(options.processArgs === undefined ? (0, host_session_1.codexProcessArgs)() : options.processArgs);
834
+ if (facts.bad)
835
+ return { official: false, reason: facts.bad };
836
+ const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? (0, node_path_1.resolve)(env.CODEX_HOME) : (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex');
837
+ const cwds = [options.processCwd ?? process.cwd(), ...(options.cwds ?? []).map(usableDir).filter((dir) => Boolean(dir)), ...facts.dirs].slice(0, MAX_ROOTS + 1);
838
+ const projectFiles = upwardPaths(cwds, (0, node_path_1.join)('.codex', 'config.toml'));
839
+ if (!projectFiles)
840
+ return { official: false, reason: 'a working directory is nested too deep' };
841
+ const files = [
842
+ (0, node_path_1.join)(home, 'config.toml'),
843
+ (0, node_path_1.join)(home, 'managed_config.toml'),
844
+ '/etc/codex/config.toml',
845
+ '/etc/codex/managed_config.toml',
846
+ ...projectFiles,
847
+ ];
848
+ const set = { names: new Map(), bad: null };
849
+ const parsedFiles = [];
850
+ let globalParsed = null;
851
+ const enabledPlugins = [];
852
+ const load = (file) => {
853
+ const result = read(file);
854
+ if (result.state === 'missing')
855
+ return true;
856
+ if (result.state === 'bad') {
857
+ set.bad = `a Codex config is ${result.reason}`;
858
+ return false;
859
+ }
860
+ try {
861
+ const parsed = parseToml(withoutBom(result.text));
862
+ if (file === (0, node_path_1.join)(home, 'config.toml'))
863
+ globalParsed = parsed;
864
+ parsedFiles.push(parsed);
865
+ return true;
866
+ }
867
+ catch {
868
+ set.bad = 'a Codex config does not parse';
869
+ return false;
870
+ }
871
+ };
872
+ for (const file of new Set(files))
873
+ if (!load(file))
874
+ return { official: false, reason: set.bad };
875
+ const profileNames = new Set();
876
+ if (facts.profile !== null) {
877
+ if (!PROFILE_NAME.test(facts.profile))
878
+ return { official: false, reason: 'unknown codex profile' };
879
+ profileNames.add(facts.profile);
880
+ }
881
+ for (const parsed of parsedFiles)
882
+ if (typeof parsed.profile === 'string')
883
+ profileNames.add(parsed.profile);
884
+ for (const name of profileNames) {
885
+ if (!PROFILE_NAME.test(name))
886
+ return { official: false, reason: 'unknown codex profile' };
887
+ if (!load((0, node_path_1.join)(home, `${name}.config.toml`)))
888
+ return { official: false, reason: set.bad };
889
+ }
890
+ for (const parsed of parsedFiles) {
891
+ if (parsed.project_root_markers !== undefined)
892
+ return { official: false, reason: 'project_root_markers changes where codex looks for project configs' };
893
+ addServers(set, parsed.mcp_servers, 'mcp_servers');
894
+ const profiles = parsed.profiles;
895
+ if (profiles !== undefined) {
896
+ if (!profiles || typeof profiles !== 'object' || Array.isArray(profiles))
897
+ return { official: false, reason: 'profiles is not a table' };
898
+ for (const profile of Object.values(profiles)) {
899
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile))
900
+ return { official: false, reason: 'a profile is not a table' };
901
+ addServers(set, profile.mcp_servers, 'profile mcp_servers');
902
+ if (profile.plugins !== undefined)
903
+ return { official: false, reason: 'a profile configures plugins' };
904
+ }
905
+ }
906
+ const plugins = parsed.plugins;
907
+ if (plugins !== undefined) {
908
+ if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins))
909
+ return { official: false, reason: 'plugins is not a table' };
910
+ for (const [id, plugin] of Object.entries(plugins)) {
911
+ if (!plugin || typeof plugin !== 'object' || Array.isArray(plugin))
912
+ return { official: false, reason: 'a Codex plugin entry is not a table' };
913
+ if (plugin.enabled === false)
914
+ continue;
915
+ // `codex plugin add` enables plugins in $CODEX_HOME/config.toml; anywhere else is not proven.
916
+ if (parsed !== globalParsed)
917
+ return { official: false, reason: 'a Codex plugin is enabled outside the user config' };
918
+ if (!Object.keys(plugin).every((key) => key === 'enabled') || (plugin.enabled !== undefined && plugin.enabled !== true)) {
919
+ return { official: false, reason: 'a Codex plugin entry has unknown settings' };
920
+ }
921
+ enabledPlugins.push(id);
922
+ }
923
+ }
924
+ }
925
+ const scan = (0, codex_plugins_1.scanCodexPlugins)({
926
+ codexHome: home,
927
+ ids: enabledPlugins,
928
+ processStartMs: options.processStartMs === undefined ? (enabledPlugins.length ? (0, host_session_1.codexProcessStartMs)() : null) : options.processStartMs,
929
+ });
930
+ if (scan.bad !== null)
931
+ return { official: false, reason: scan.bad };
932
+ let officialPlugin = 0;
933
+ for (const plugin of scan.plugins) {
934
+ for (const name of plugin.servers) {
935
+ if (!codexServerNameCanProduceMarrowLabel(name))
936
+ continue;
937
+ if (!plugin.official)
938
+ return { official: false, reason: 'a Codex plugin declares a server that can produce the marrow label' };
939
+ officialPlugin += 1;
940
+ }
941
+ }
942
+ if (officialPlugin > 1)
943
+ return { official: false, reason: 'more than one Codex plugin declares the marrow server' };
944
+ if (officialPlugin === 1) {
945
+ // The official plugin is the only source of the label: no configured server may produce it too.
946
+ if (set.bad)
947
+ return { official: false, reason: set.bad };
948
+ if ([...set.names.keys()].some(codexServerNameCanProduceMarrowLabel))
949
+ return { official: false, reason: 'the official plugin and another server can both produce the marrow label' };
950
+ return { official: true, reason: 'official plugin' };
951
+ }
952
+ return decide(set, CODEX_SERVER_KEYS, false, cwds, { env, home: options.userHome ?? (0, node_os_1.homedir)() }, true);
953
+ }
954
+ /** Whether Cursor's `marrow` server is provably the official one (and, at preToolUse, the only server). */
955
+ function cursorOfficialMarrowServer(options) {
956
+ const read = options.read ?? readHostConfigFile;
957
+ const home = options.home ?? (0, node_os_1.homedir)();
958
+ const cwds = [options.processCwd ?? process.cwd(), ...(options.cwds ?? []).map(usableDir).filter((dir) => Boolean(dir))].slice(0, MAX_ROOTS + 1);
959
+ const projectFiles = upwardPaths(cwds, (0, node_path_1.join)('.cursor', 'mcp.json'));
960
+ if (!projectFiles)
961
+ return { official: false, reason: 'a workspace root is nested too deep' };
962
+ const files = new Set([(0, node_path_1.join)(home, '.cursor', 'mcp.json'), ...projectFiles]);
963
+ const set = { names: new Map(), bad: null };
964
+ for (const file of files) {
965
+ const result = read(file);
966
+ if (result.state === 'missing')
967
+ continue;
968
+ if (result.state === 'bad')
969
+ return { official: false, reason: `a Cursor MCP config is ${result.reason}` };
970
+ let parsed;
971
+ try {
972
+ parsed = JSON.parse(withoutBom(result.text));
973
+ }
974
+ catch {
975
+ return { official: false, reason: 'a Cursor MCP config does not parse' };
976
+ }
977
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
978
+ return { official: false, reason: 'a Cursor MCP config is not an object' };
979
+ addServers(set, parsed.mcpServers, 'mcpServers');
980
+ }
981
+ return decide(set, CURSOR_SERVER_KEYS, options.onlyServer, cwds, { env: options.env ?? process.env, home });
982
+ }
983
+ // ---------------------------------------------------------------------------
984
+ // The host-aware own-tool decision
985
+ // ---------------------------------------------------------------------------
986
+ const memo = new Map();
987
+ /** Test seam: forget the per-process config decision. */
988
+ function resetOwnServerIdentityCache() {
989
+ memo.clear();
990
+ }
991
+ function rawRecord(value) {
992
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
993
+ }
994
+ function exactOwnLabel(label, prefix) {
995
+ return typeof label === 'string' && label.startsWith(prefix) && own_tool_1.MARROW_OWN_TOOL_NAMES.has(label.slice(prefix.length));
996
+ }
997
+ /**
998
+ * Marrow's own tool on this host, after the server identity check. `event` is
999
+ * the normalised event; `raw` is the host's payload as received (for Cursor's
1000
+ * `url` and `workspace_roots`, Codex's `cwd`). Hosts other than Codex and
1001
+ * Cursor keep the label rule (isOfficialMarrowMcpEvent) unchanged.
1002
+ */
1003
+ function isOfficialMarrowMcpEventOnHost(harness, event, raw) {
1004
+ if (harness !== 'codex' && harness !== 'cursor')
1005
+ return (0, own_tool_1.isOfficialMarrowMcpEvent)(event);
1006
+ const payload = rawRecord(raw ?? event);
1007
+ if (harness === 'codex') {
1008
+ if (!exactOwnLabel(event.tool_name, 'mcp__marrow__'))
1009
+ return false;
1010
+ const cwd = usableDir(payload.cwd);
1011
+ const key = `codex\0${process.env.HOME ?? ''}\0${process.env.CODEX_HOME ?? ''}\0${cwd ?? ''}`;
1012
+ if (!memo.has(key)) {
1013
+ let verdict;
1014
+ try {
1015
+ verdict = codexOfficialMarrowServer({ cwds: cwd ? [cwd] : [] });
1016
+ }
1017
+ catch {
1018
+ verdict = { official: false, reason: 'identity check failed' };
1019
+ }
1020
+ memo.set(key, verdict);
1021
+ }
1022
+ return memo.get(key).official;
1023
+ }
1024
+ // Cursor: beforeMCPExecution is normalised to mcp__marrow__<own> only for server exactly `marrow`;
1025
+ // preToolUse carries `MCP:<tool>` with no server.
1026
+ const beforeMcp = exactOwnLabel(event.tool_name, 'mcp__marrow__');
1027
+ const preToolUse = exactOwnLabel(event.tool_name, 'MCP:') && !String(event.tool_name).startsWith('MCP:marrow:');
1028
+ if (!beforeMcp && !preToolUse)
1029
+ return false;
1030
+ if (beforeMcp && payload.url !== undefined && payload.url !== null && payload.url !== '')
1031
+ return false;
1032
+ // More roots than the bound, a non-array, or any root that is not a usable absolute path proves nothing.
1033
+ const given = payload.workspace_roots;
1034
+ if (given !== undefined && given !== null && !Array.isArray(given))
1035
+ return false;
1036
+ const roots = Array.isArray(given) ? given : [];
1037
+ if (roots.length > MAX_ROOTS || !roots.every((root) => usableDir(root) !== null))
1038
+ return false;
1039
+ const key = `cursor\0${process.env.HOME ?? ''}\0${beforeMcp ? 'server' : 'only'}\0${roots.map(String).join('\0')}`;
1040
+ if (!memo.has(key)) {
1041
+ let verdict;
1042
+ try {
1043
+ verdict = cursorOfficialMarrowServer({ cwds: roots, onlyServer: !beforeMcp });
1044
+ }
1045
+ catch {
1046
+ verdict = { official: false, reason: 'identity check failed' };
1047
+ }
1048
+ memo.set(key, verdict);
1049
+ }
1050
+ return memo.get(key).official;
1051
+ }
1052
+ //# sourceMappingURL=own-server-identity.js.map