@getmarrow/mcp 3.9.102 → 3.9.106

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