@getmarrow/mcp 3.9.101 → 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 (127) hide show
  1. package/README.md +63 -29
  2. package/dist/cli.js +64 -1005
  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/env.d.ts +2 -0
  13. package/dist/env.d.ts.map +1 -1
  14. package/dist/env.js +229 -39
  15. package/dist/env.js.map +1 -1
  16. package/dist/hook-contract.d.ts +41 -3
  17. package/dist/hook-contract.d.ts.map +1 -1
  18. package/dist/hook-contract.js +126 -18
  19. package/dist/hook-contract.js.map +1 -1
  20. package/dist/hook-pre-action.d.ts +48 -2
  21. package/dist/hook-pre-action.d.ts.map +1 -1
  22. package/dist/hook-pre-action.js +199 -23
  23. package/dist/hook-pre-action.js.map +1 -1
  24. package/dist/hook-tool-policy.d.ts +3 -3
  25. package/dist/hook-tool-policy.d.ts.map +1 -1
  26. package/dist/hook-tool-policy.js +6 -22
  27. package/dist/hook-tool-policy.js.map +1 -1
  28. package/dist/hook.d.ts +2 -2
  29. package/dist/hook.d.ts.map +1 -1
  30. package/dist/hook.js +13 -6
  31. package/dist/hook.js.map +1 -1
  32. package/dist/host-approval-state.d.ts +9 -0
  33. package/dist/host-approval-state.d.ts.map +1 -1
  34. package/dist/host-approval-state.js +4 -0
  35. package/dist/host-approval-state.js.map +1 -1
  36. package/dist/host-approval.d.ts +6 -0
  37. package/dist/host-approval.d.ts.map +1 -1
  38. package/dist/host-approval.js +3 -1
  39. package/dist/host-approval.js.map +1 -1
  40. package/dist/host-session.d.ts +11 -0
  41. package/dist/host-session.d.ts.map +1 -1
  42. package/dist/host-session.js +49 -0
  43. package/dist/host-session.js.map +1 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +27 -19
  46. package/dist/index.js.map +1 -1
  47. package/dist/normalized-action.d.ts +29 -6
  48. package/dist/normalized-action.d.ts.map +1 -1
  49. package/dist/normalized-action.js +497 -103
  50. package/dist/normalized-action.js.map +1 -1
  51. package/dist/own-server-identity.d.ts +123 -0
  52. package/dist/own-server-identity.d.ts.map +1 -0
  53. package/dist/own-server-identity.js +1052 -0
  54. package/dist/own-server-identity.js.map +1 -0
  55. package/dist/own-tool.d.ts +87 -0
  56. package/dist/own-tool.d.ts.map +1 -0
  57. package/dist/own-tool.js +169 -0
  58. package/dist/own-tool.js.map +1 -0
  59. package/dist/redact.d.ts +23 -0
  60. package/dist/redact.d.ts.map +1 -1
  61. package/dist/redact.js +181 -8
  62. package/dist/redact.js.map +1 -1
  63. package/dist/seatbelt/classifier-pin.d.ts +15 -0
  64. package/dist/seatbelt/classifier-pin.d.ts.map +1 -0
  65. package/dist/seatbelt/classifier-pin.js +26 -0
  66. package/dist/seatbelt/classifier-pin.js.map +1 -0
  67. package/dist/seatbelt/guards.d.ts +10 -0
  68. package/dist/seatbelt/guards.d.ts.map +1 -0
  69. package/dist/seatbelt/guards.js +509 -0
  70. package/dist/seatbelt/guards.js.map +1 -0
  71. package/dist/seatbelt/hook.d.ts +72 -0
  72. package/dist/seatbelt/hook.d.ts.map +1 -0
  73. package/dist/seatbelt/hook.js +362 -0
  74. package/dist/seatbelt/hook.js.map +1 -0
  75. package/dist/seatbelt/longopts.d.ts +68 -0
  76. package/dist/seatbelt/longopts.d.ts.map +1 -0
  77. package/dist/seatbelt/longopts.js +377 -0
  78. package/dist/seatbelt/longopts.js.map +1 -0
  79. package/dist/seatbelt/mapper.d.ts +83 -0
  80. package/dist/seatbelt/mapper.d.ts.map +1 -0
  81. package/dist/seatbelt/mapper.js +10248 -0
  82. package/dist/seatbelt/mapper.js.map +1 -0
  83. package/dist/seatbelt/record.d.ts +94 -0
  84. package/dist/seatbelt/record.d.ts.map +1 -0
  85. package/dist/seatbelt/record.js +776 -0
  86. package/dist/seatbelt/record.js.map +1 -0
  87. package/dist/seatbelt/tables.d.ts +82 -0
  88. package/dist/seatbelt/tables.d.ts.map +1 -0
  89. package/dist/seatbelt/tables.js +359 -0
  90. package/dist/seatbelt/tables.js.map +1 -0
  91. package/dist/seatbelt-deploy.d.ts +100 -0
  92. package/dist/seatbelt-deploy.d.ts.map +1 -0
  93. package/dist/seatbelt-deploy.js +393 -0
  94. package/dist/seatbelt-deploy.js.map +1 -0
  95. package/dist/seatbelt-fs.d.ts +30 -0
  96. package/dist/seatbelt-fs.d.ts.map +1 -0
  97. package/dist/seatbelt-fs.js +94 -0
  98. package/dist/seatbelt-fs.js.map +1 -0
  99. package/dist/seatbelt-git.d.ts +212 -0
  100. package/dist/seatbelt-git.d.ts.map +1 -0
  101. package/dist/seatbelt-git.js +844 -0
  102. package/dist/seatbelt-git.js.map +1 -0
  103. package/dist/seatbelt-mode.d.ts +77 -0
  104. package/dist/seatbelt-mode.d.ts.map +1 -0
  105. package/dist/seatbelt-mode.js +496 -0
  106. package/dist/seatbelt-mode.js.map +1 -0
  107. package/dist/seatbelt-path.d.ts +123 -0
  108. package/dist/seatbelt-path.d.ts.map +1 -0
  109. package/dist/seatbelt-path.js +482 -0
  110. package/dist/seatbelt-path.js.map +1 -0
  111. package/dist/seatbelt-scripts.d.ts +109 -0
  112. package/dist/seatbelt-scripts.d.ts.map +1 -0
  113. package/dist/seatbelt-scripts.js +1060 -0
  114. package/dist/seatbelt-scripts.js.map +1 -0
  115. package/dist/tool-catalog.d.ts +6852 -0
  116. package/dist/tool-catalog.d.ts.map +1 -0
  117. package/dist/tool-catalog.js +999 -0
  118. package/dist/tool-catalog.js.map +1 -0
  119. package/dist/types.d.ts +2 -1
  120. package/dist/types.d.ts.map +1 -1
  121. package/package.json +2 -1
  122. package/server.json +10 -3
  123. package/vendor/classifier/MANIFEST.json +25 -0
  124. package/vendor/classifier/classifier.js +1356 -0
  125. package/vendor/classifier/policy-baseline.js +187 -0
  126. package/vendor/classifier/redact.js +59 -0
  127. package/vendor/classifier/shell-parse.js +455 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The MCP servers enabled Codex plugins add, read from the plugin cache the
3
+ * way codex 0.161 lays it out (`codex plugin add` writes
4
+ * `$CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>/` and enables
5
+ * `[plugins."<plugin>@<marketplace>"]` in `$CODEX_HOME/config.toml`), and
6
+ * whether a plugin is the official Marrow Codex plugin, proven only by the
7
+ * SHA-256 of every file that decides what its `marrow` server runs.
8
+ *
9
+ * Fail closed: a missing cache, more than one version, a symlink whose real
10
+ * path leaves the cache, a file that is not a regular file, over the size cap
11
+ * or unparsable, an unknown `mcpServers` shape, a plugin file changed after
12
+ * codex started (or no codex process to compare with) means "not proven".
13
+ * Nothing read here is ever logged.
14
+ */
15
+ export type PluginServers = {
16
+ id: string;
17
+ official: boolean;
18
+ servers: string[];
19
+ };
20
+ export type PluginScan = {
21
+ bad: string;
22
+ } | {
23
+ bad: null;
24
+ plugins: PluginServers[];
25
+ };
26
+ export type PluginScanOptions = {
27
+ codexHome: string;
28
+ ids: string[];
29
+ /** Wall-clock start of the codex process (ms), or null when it cannot be found. */
30
+ processStartMs: number | null;
31
+ };
32
+ /** Every enabled plugin's MCP server names; any doubt is `bad`. */
33
+ export declare function scanCodexPlugins(options: PluginScanOptions): PluginScan;
34
+ //# sourceMappingURL=codex-plugins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-plugins.d.ts","sourceRoot":"","sources":["../src/codex-plugins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AACjF,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC;AAkJnF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,mFAAmF;IACnF,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CA6BvE"}
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ /**
3
+ * The MCP servers enabled Codex plugins add, read from the plugin cache the
4
+ * way codex 0.161 lays it out (`codex plugin add` writes
5
+ * `$CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>/` and enables
6
+ * `[plugins."<plugin>@<marketplace>"]` in `$CODEX_HOME/config.toml`), and
7
+ * whether a plugin is the official Marrow Codex plugin, proven only by the
8
+ * SHA-256 of every file that decides what its `marrow` server runs.
9
+ *
10
+ * Fail closed: a missing cache, more than one version, a symlink whose real
11
+ * path leaves the cache, a file that is not a regular file, over the size cap
12
+ * or unparsable, an unknown `mcpServers` shape, a plugin file changed after
13
+ * codex started (or no codex process to compare with) means "not proven".
14
+ * Nothing read here is ever logged.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.scanCodexPlugins = scanCodexPlugins;
18
+ const node_crypto_1 = require("node:crypto");
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = require("node:path");
21
+ const own_server_identity_1 = require("./own-server-identity");
22
+ const codex_plugin_pins_1 = require("./codex-plugin-pins");
23
+ const MAX_PLUGINS = 32;
24
+ /** codex keeps whole-second precision in places and the process start is exact to about a second. */
25
+ const START_MARGIN_MS = 2_000;
26
+ const PLUGIN_ID = /^([A-Za-z0-9_][A-Za-z0-9_.-]{0,127})@([A-Za-z0-9_][A-Za-z0-9_.-]{0,127})$/;
27
+ const MANIFESTS = ['.codex-plugin/plugin.json', '.claude-plugin/plugin.json', '.cursor-plugin/plugin.json'];
28
+ const DEFAULT_MCP_FILE = '.mcp.json';
29
+ function insideRoot(root, real) {
30
+ return real.startsWith(`${root}${node_path_1.sep}`);
31
+ }
32
+ /** A file inside the plugin version directory: relative, no `..`, real path inside the cache. */
33
+ function readPluginFile(at, rel) {
34
+ if (typeof rel !== 'string' || rel.length === 0 || rel.length > 1024 || rel.includes('\0') || (0, node_path_1.isAbsolute)(rel))
35
+ return { state: 'bad' };
36
+ const clean = (0, node_path_1.normalize)(rel);
37
+ if (clean === '..' || clean.startsWith(`..${node_path_1.sep}`) || (0, node_path_1.isAbsolute)(clean))
38
+ return { state: 'bad' };
39
+ const path = (0, node_path_1.join)(at.dir, clean);
40
+ let real;
41
+ try {
42
+ real = (0, node_fs_1.realpathSync)(path);
43
+ }
44
+ catch (error) {
45
+ const code = error?.code;
46
+ return code === 'ENOENT' || code === 'ENOTDIR' ? { state: 'missing' } : { state: 'bad' };
47
+ }
48
+ if (!insideRoot(at.root, real))
49
+ return { state: 'bad' };
50
+ const read = (0, own_server_identity_1.readHostConfigBytes)(real);
51
+ if (read.state !== 'ok')
52
+ return read.state === 'missing' ? { state: 'missing' } : { state: 'bad' };
53
+ try {
54
+ at.newest = Math.max(at.newest, (0, node_fs_1.statSync)(real).mtimeMs, (0, node_fs_1.lstatSync)(path).mtimeMs);
55
+ }
56
+ catch {
57
+ return { state: 'bad' };
58
+ }
59
+ return read;
60
+ }
61
+ function withoutBom(text) {
62
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
63
+ }
64
+ function plainObject(value) {
65
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
66
+ }
67
+ /** `{ "mcpServers": { name: {...} } }` or a bare `{ name: {...} }` map; anything else is unknown. */
68
+ function serverNames(value) {
69
+ if (!plainObject(value))
70
+ return null;
71
+ const keys = Object.keys(value);
72
+ if (keys.includes('mcpServers')) {
73
+ if (keys.length !== 1 || !plainObject(value.mcpServers))
74
+ return null;
75
+ value = value.mcpServers;
76
+ }
77
+ const map = value;
78
+ if (!Object.values(map).every(plainObject))
79
+ return null;
80
+ return Object.keys(map);
81
+ }
82
+ function parseJson(text) {
83
+ try {
84
+ return JSON.parse(withoutBom(text));
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
90
+ function sha256(bytes) {
91
+ return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
92
+ }
93
+ /** One enabled plugin's servers, and whether its files are exactly a pinned official release. */
94
+ function scanPlugin(cacheRoot, realRoot, id) {
95
+ const match = PLUGIN_ID.exec(id);
96
+ if (!match)
97
+ return 'a Codex plugin id has an unknown form';
98
+ const [, plugin, marketplace] = match;
99
+ const pluginDir = (0, node_path_1.join)(cacheRoot, marketplace, plugin);
100
+ let versions;
101
+ try {
102
+ versions = (0, node_fs_1.readdirSync)(pluginDir);
103
+ }
104
+ catch {
105
+ return 'an enabled Codex plugin is not in the local cache';
106
+ }
107
+ if (versions.length !== 1)
108
+ return versions.length ? 'more than one version of a Codex plugin is cached' : 'an enabled Codex plugin is not in the local cache';
109
+ const dir = (0, node_path_1.join)(pluginDir, versions[0]);
110
+ let realDir;
111
+ try {
112
+ realDir = (0, node_fs_1.realpathSync)(dir);
113
+ if (!(0, node_fs_1.statSync)(realDir).isDirectory())
114
+ return 'a Codex plugin cache entry is not a directory';
115
+ }
116
+ catch {
117
+ return 'a Codex plugin cache entry is unreadable';
118
+ }
119
+ if (!insideRoot(realRoot, realDir))
120
+ return 'a Codex plugin cache entry leaves the cache';
121
+ const at = { root: realRoot, dir, newest: 0 };
122
+ try {
123
+ at.newest = Math.max((0, node_fs_1.lstatSync)(dir).mtimeMs, (0, node_fs_1.statSync)(realDir).mtimeMs);
124
+ }
125
+ catch {
126
+ return 'a Codex plugin cache entry is unreadable';
127
+ }
128
+ const names = new Set();
129
+ const texts = new Map();
130
+ const take = (rel, required) => {
131
+ const read = readPluginFile(at, rel);
132
+ if (read.state === 'missing')
133
+ return required ? 'a Codex plugin file is missing' : null;
134
+ if (read.state === 'bad')
135
+ return 'a Codex plugin file is unreadable, not a regular file, too large or outside the cache';
136
+ texts.set((0, node_path_1.normalize)(rel), read.bytes);
137
+ const parsed = parseJson(read.bytes.toString('utf8'));
138
+ return parsed === undefined ? 'a Codex plugin file does not parse' : { parsed };
139
+ };
140
+ const manifests = [];
141
+ for (const rel of MANIFESTS) {
142
+ const got = take(rel, false);
143
+ if (typeof got === 'string')
144
+ return got;
145
+ if (got === null)
146
+ continue;
147
+ const manifest = got.parsed;
148
+ if (!plainObject(manifest))
149
+ return 'a Codex plugin manifest is not an object';
150
+ manifests.push(manifest);
151
+ const servers = manifest.mcpServers;
152
+ if (servers === undefined)
153
+ continue;
154
+ let found;
155
+ if (typeof servers === 'string') {
156
+ const file = take(servers, true);
157
+ if (typeof file === 'string')
158
+ return file;
159
+ found = serverNames(file.parsed);
160
+ }
161
+ else {
162
+ found = serverNames(servers);
163
+ }
164
+ if (!found)
165
+ return 'a Codex plugin declares MCP servers in an unknown shape';
166
+ for (const name of found)
167
+ names.add(name);
168
+ }
169
+ if (!manifests.length)
170
+ return 'a Codex plugin has no manifest';
171
+ const fallback = take(DEFAULT_MCP_FILE, false);
172
+ if (typeof fallback === 'string')
173
+ return fallback;
174
+ if (fallback !== null) {
175
+ const found = serverNames(fallback.parsed);
176
+ if (!found)
177
+ return 'a Codex plugin declares MCP servers in an unknown shape';
178
+ for (const name of found)
179
+ names.add(name);
180
+ }
181
+ // The official plugin: one codex manifest naming a pinned release, cached under its own version,
182
+ // and every file that decides what its server runs byte-identical to that release.
183
+ let official = false;
184
+ const primary = manifests.length === 1 && texts.has((0, node_path_1.normalize)(MANIFESTS[0])) ? manifests[0] : null;
185
+ const pins = primary && typeof primary.name === 'string' && typeof primary.version === 'string'
186
+ ? codex_plugin_pins_1.OFFICIAL_CODEX_PLUGIN_PINS[`${primary.name}@${primary.version}`]
187
+ : undefined;
188
+ if (pins && (0, node_path_1.basename)(dir) === primary.version) {
189
+ official = Object.entries(pins).every(([rel, digest]) => {
190
+ if (!texts.has((0, node_path_1.normalize)(rel))) {
191
+ const read = readPluginFile(at, rel);
192
+ if (read.state !== 'ok')
193
+ return false;
194
+ texts.set((0, node_path_1.normalize)(rel), read.bytes);
195
+ }
196
+ return sha256(texts.get((0, node_path_1.normalize)(rel))) === digest;
197
+ });
198
+ }
199
+ return { id, official, servers: [...names], newest: at.newest };
200
+ }
201
+ /** Every enabled plugin's MCP server names; any doubt is `bad`. */
202
+ function scanCodexPlugins(options) {
203
+ if (!options.ids.length)
204
+ return { bad: null, plugins: [] };
205
+ if (options.ids.length > MAX_PLUGINS)
206
+ return { bad: 'too many Codex plugins are enabled' };
207
+ if (options.processStartMs === null || !Number.isFinite(options.processStartMs)) {
208
+ return { bad: 'the codex process start is unknown, so plugin files cannot be dated' };
209
+ }
210
+ const plugins = (0, node_path_1.join)(options.codexHome, 'plugins');
211
+ try {
212
+ const staging = (0, node_fs_1.readdirSync)((0, node_path_1.join)(plugins, '.remote-plugin-install-staging'));
213
+ if (staging.length)
214
+ return { bad: 'a remote Codex plugin install is staged' };
215
+ }
216
+ catch (error) {
217
+ if (error?.code !== 'ENOENT')
218
+ return { bad: 'the Codex plugin staging folder is unreadable' };
219
+ }
220
+ const cacheRoot = (0, node_path_1.join)(plugins, 'cache');
221
+ let realRoot;
222
+ try {
223
+ realRoot = (0, node_fs_1.realpathSync)(cacheRoot);
224
+ }
225
+ catch {
226
+ return { bad: 'the Codex plugin cache is missing' };
227
+ }
228
+ const out = [];
229
+ for (const id of options.ids) {
230
+ const scanned = scanPlugin(cacheRoot, realRoot, id);
231
+ if (typeof scanned === 'string')
232
+ return { bad: scanned };
233
+ const { newest, ...plugin } = scanned;
234
+ if (!(newest < options.processStartMs - START_MARGIN_MS))
235
+ return { bad: 'a Codex plugin changed after codex started' };
236
+ out.push(plugin);
237
+ }
238
+ return { bad: null, plugins: out };
239
+ }
240
+ //# sourceMappingURL=codex-plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-plugins.js","sourceRoot":"","sources":["../src/codex-plugins.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface ResolvedMarrowEnv {
7
7
  missing: boolean;
8
8
  exactFix: string;
9
9
  }
10
+ /** Host (with port) of a non-default base URL, for status output; null for the default. */
11
+ export declare function nonDefaultBaseUrlHost(baseUrl: string): string | null;
10
12
  export declare function resolveMarrowEnv(options?: {
11
13
  cwd?: string;
12
14
  env?: NodeJS.ProcessEnv;
package/dist/env.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6FD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,iBAAiB,CAsDzB"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQpE;AAiLD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,iBAAiB,CA6DzB"}
package/dist/env.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.nonDefaultBaseUrlHost = nonDefaultBaseUrlHost;
36
37
  exports.resolveMarrowEnv = resolveMarrowEnv;
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
@@ -46,6 +47,47 @@ const ALLOWED_ENV_KEYS = new Set([
46
47
  'MARROW_AGENT_ID',
47
48
  'MARROW_SESSION_ID',
48
49
  ]);
50
+ const MAX_ENV_FILE_BYTES = 1024 * 1024;
51
+ const REPO_KEY_FIX = 'run npx @getmarrow/install doctor, or move the line to ~/.marrow/env, then chmod 600 ~/.marrow/env';
52
+ /** Host (with port) of a non-default base URL, for status output; null for the default. */
53
+ function nonDefaultBaseUrlHost(baseUrl) {
54
+ const normalized = normalizeBaseUrl(baseUrl);
55
+ if (normalized === normalizeBaseUrl(DEFAULT_BASE_URL))
56
+ return null;
57
+ try {
58
+ return new URL(baseUrl).host || 'invalid';
59
+ }
60
+ catch {
61
+ return 'invalid';
62
+ }
63
+ }
64
+ function normalizeBaseUrl(value) {
65
+ try {
66
+ return new URL(String(value || '').trim()).href.replace(/\/+$/, '');
67
+ }
68
+ catch {
69
+ return `invalid:${String(value || '')}`;
70
+ }
71
+ }
72
+ // File paths come from the working directory; keep notices on one printable line.
73
+ const MAX_PRINTED_PATH = 200;
74
+ // Paths in notices can be attacker-named: control, line-separator and format
75
+ // (bidi, zero-width) characters become '?', and the length is capped.
76
+ function printablePath(filePath) {
77
+ const clean = filePath.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
78
+ const chars = Array.from(clean);
79
+ return chars.length > MAX_PRINTED_PATH ? `${chars.slice(0, MAX_PRINTED_PATH - 3).join('')}...` : clean;
80
+ }
81
+ // Shell operand for a chmod command built from the REAL path. Returns null when
82
+ // the path cannot be printed faithfully (control/format/line-separator characters
83
+ // or longer than the print cap), so advice never targets a shortened or '?' path.
84
+ function shellOperand(realPath) {
85
+ if (/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(realPath) || Array.from(realPath).length > MAX_PRINTED_PATH)
86
+ return null;
87
+ if (/^[A-Za-z0-9_@%+=:,./][A-Za-z0-9_@%+=:,./-]*$/.test(realPath))
88
+ return realPath;
89
+ return `'${realPath.replace(/'/g, `'\\''`)}'`;
90
+ }
49
91
  function stripQuotes(value) {
50
92
  const trimmed = String(value || '').trim();
51
93
  if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
@@ -53,35 +95,84 @@ function stripQuotes(value) {
53
95
  }
54
96
  return trimmed;
55
97
  }
56
- function isPrivateOwnerFile(filePath, home) {
98
+ // The one trust decision for owner files, shared by the MCP and hook paths. It
99
+ // is made about the bytes that were read: the file is opened without following
100
+ // a symlink, its type, owner and mode come from fstat on that same descriptor,
101
+ // and an lstat of the path taken after the read must name the same inode. The
102
+ // file must be the owner's and private; home and ~/.marrow must be the owner's
103
+ // real directories, not writable by anyone else. Anything else supplies nothing.
104
+ function readOwnerEnvFile(filePath, home) {
105
+ const resolvedHome = path.resolve(home);
106
+ const resolvedFile = path.resolve(filePath);
107
+ const read = readEnvFile(resolvedFile, true, Infinity);
108
+ if (!read) {
109
+ try {
110
+ fs.lstatSync(resolvedFile);
111
+ return { status: 'rejected' };
112
+ }
113
+ catch {
114
+ return { status: 'absent' };
115
+ }
116
+ }
57
117
  try {
58
- const resolvedHome = path.resolve(home);
59
- const resolvedFile = path.resolve(filePath);
60
118
  const parent = path.dirname(resolvedFile);
61
119
  const homeStat = fs.lstatSync(resolvedHome);
62
120
  const parentStat = fs.lstatSync(parent);
63
- const fileStat = fs.lstatSync(resolvedFile);
121
+ const pathStat = fs.lstatSync(resolvedFile);
64
122
  const uid = typeof process.getuid === 'function' ? process.getuid() : null;
65
- return resolvedFile.startsWith(`${resolvedHome}${path.sep}`)
66
- && homeStat.isDirectory() && !homeStat.isSymbolicLink() && fs.realpathSync(resolvedHome) === resolvedHome
67
- && parentStat.isDirectory() && !parentStat.isSymbolicLink() && fs.realpathSync(parent) === parent
68
- && fileStat.isFile() && !fileStat.isSymbolicLink()
69
- && (uid === null || fileStat.uid === uid && parentStat.uid === uid && homeStat.uid === uid)
70
- && (fileStat.mode & 0o077) === 0
71
- && (parentStat.mode & 0o022) === 0
72
- && (homeStat.mode & 0o022) === 0;
123
+ const owned = (stat) => uid === null || stat.uid === uid;
124
+ const safeDir = (stat, dir) => stat.isDirectory() && !stat.isSymbolicLink()
125
+ && fs.realpathSync(dir) === dir && owned(stat) && (stat.mode & 0o022) === 0;
126
+ const trusted = resolvedFile.startsWith(`${resolvedHome}${path.sep}`)
127
+ && safeDir(homeStat, resolvedHome) && safeDir(parentStat, parent)
128
+ && read.stat.isFile() && owned(read.stat) && (read.stat.mode & 0o077) === 0
129
+ && pathStat.isFile() && pathStat.dev === read.stat.dev && pathStat.ino === read.stat.ino;
130
+ return trusted ? { status: 'trusted', values: read.values } : { status: 'rejected' };
73
131
  }
74
132
  catch {
75
- return false;
133
+ return { status: 'rejected' };
76
134
  }
77
135
  }
78
- function parseEnvFile(filePath, trustedHome) {
79
- if (!fs.existsSync(filePath))
80
- return {};
81
- if (trustedHome && !isPrivateOwnerFile(filePath, trustedHome))
82
- return {};
136
+ // Opens without blocking (a planted FIFO cannot hang the server) and reads only
137
+ // regular files. Only untrusted repo files are size-capped (maxBytes); owner files
138
+ // keep base parity (no cap), so a file base accepted is never turned into no-key.
139
+ // noFollow refuses a symlink at open time where supported.
140
+ function readEnvFile(filePath, noFollow, maxBytes) {
141
+ const flags = fs.constants.O_RDONLY
142
+ | (fs.constants.O_NONBLOCK || 0)
143
+ | (noFollow ? fs.constants.O_NOFOLLOW || 0 : 0);
144
+ let fd;
145
+ try {
146
+ fd = fs.openSync(filePath, flags);
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ try {
152
+ const stat = fs.fstatSync(fd);
153
+ if (!stat.isFile() || stat.size > maxBytes)
154
+ return null;
155
+ const buffer = Buffer.alloc(stat.size + 1);
156
+ let length = 0;
157
+ while (length < buffer.length) {
158
+ const read = fs.readSync(fd, buffer, length, buffer.length - length, null);
159
+ if (read <= 0)
160
+ break;
161
+ length += read;
162
+ }
163
+ if (length > stat.size)
164
+ return null;
165
+ return { values: parseEnvText(buffer.subarray(0, length).toString('utf8')), stat };
166
+ }
167
+ catch {
168
+ return null;
169
+ }
170
+ finally {
171
+ fs.closeSync(fd);
172
+ }
173
+ }
174
+ function parseEnvText(raw) {
83
175
  const values = {};
84
- const raw = fs.readFileSync(filePath, 'utf8');
85
176
  for (const line of raw.split(/\r?\n/)) {
86
177
  const match = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
87
178
  if (!match)
@@ -96,14 +187,14 @@ function parseEnvFile(filePath, trustedHome) {
96
187
  }
97
188
  return values;
98
189
  }
99
- function candidateEnvFiles(cwd, home, trustedOnly) {
100
- const ownerFiles = [
190
+ function ownerEnvFiles(home) {
191
+ return [
101
192
  path.join(home, '.marrow', 'env.local'),
102
193
  path.join(home, '.marrow', 'env'),
103
194
  ];
104
- if (trustedOnly)
105
- return ownerFiles;
106
- const files = [...ownerFiles];
195
+ }
196
+ function repoEnvFiles(cwd, ownerFiles) {
197
+ const files = [];
107
198
  let dir = path.resolve(cwd || process.cwd());
108
199
  for (let depth = 0; depth < 8; depth += 1) {
109
200
  files.push(path.join(dir, '.marrow', 'env.local'));
@@ -115,7 +206,7 @@ function candidateEnvFiles(cwd, home, trustedOnly) {
115
206
  break;
116
207
  dir = parent;
117
208
  }
118
- return [...new Set(files)];
209
+ return [...new Set(files)].filter((file) => !ownerFiles.includes(file));
119
210
  }
120
211
  function pickKey(env) {
121
212
  if (env.MARROW_API_KEY)
@@ -169,21 +260,119 @@ function resolveMarrowEnv(options = {}) {
169
260
  exactFix: 'Marrow key is loaded from the process environment.',
170
261
  };
171
262
  }
172
- for (const filePath of candidateEnvFiles(cwd, home, options.trustedOnly === true)) {
173
- const parsed = parseEnvFile(filePath, options.trustedOnly === true ? home : undefined);
174
- const found = pickKey(parsed);
175
- if (!found.key)
263
+ // Owner files, in the same order and with the same check on the MCP and hook
264
+ // paths. A file that fails the check supplies nothing, key included.
265
+ const ownerFiles = ownerEnvFiles(home);
266
+ let ignored = null;
267
+ for (const filePath of ownerFiles) {
268
+ const read = readOwnerEnvFile(filePath, home);
269
+ if (read.status === 'rejected') {
270
+ if (!ignored)
271
+ ignored = ownerFileNotice(filePath, home);
176
272
  continue;
177
- return {
178
- apiKey: found.key,
179
- baseUrl: parsed.MARROW_BASE_URL || env.MARROW_BASE_URL || DEFAULT_BASE_URL,
180
- agentId: parsed.MARROW_FLEET_AGENT_ID || parsed.MARROW_AGENT_ID || env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID,
181
- sessionId: parsed.MARROW_SESSION_ID || env.MARROW_SESSION_ID,
182
- source: `${filePath}:${found.source}`,
183
- missing: false,
184
- exactFix: `Marrow key was found in ${filePath}. Keep this file private and run npx @getmarrow/install doctor to verify.`,
185
- };
273
+ }
274
+ if (read.status !== 'trusted')
275
+ continue;
276
+ const found = pickKey(read.values);
277
+ if (found.key)
278
+ return fromFile(filePath, found, read.values, env);
279
+ }
280
+ if (options.trustedOnly === true)
281
+ return missingKey(env, ignored);
282
+ // Repository-level files (found by walking up from cwd) never supply a key or
283
+ // any other Marrow setting, whatever their owner or mode: a umask-077 clone or a
284
+ // mode-preserving archive yields a private file the owner never wrote. They are
285
+ // read (bounded, non-blocking) only to tell the owner a key there was skipped.
286
+ for (const filePath of repoEnvFiles(cwd, ownerFiles)) {
287
+ try {
288
+ fs.lstatSync(filePath);
289
+ }
290
+ catch {
291
+ continue;
292
+ }
293
+ const peek = readEnvFile(filePath, false, MAX_ENV_FILE_BYTES);
294
+ if (pickKey(peek?.values || {}).key && !ignored)
295
+ ignored = repoKeyNotice(filePath);
296
+ }
297
+ return missingKey(env, ignored);
298
+ }
299
+ function repoKeyNotice(filePath) {
300
+ return `Marrow key in ${printablePath(filePath)} ignored: keys are read only from ~/.marrow/env or your environment. Fix: ${REPO_KEY_FIX}.`;
301
+ }
302
+ function symlinkInChain(filePath) {
303
+ try {
304
+ if (fs.lstatSync(filePath).isSymbolicLink())
305
+ return 'file';
306
+ }
307
+ catch {
308
+ // Missing or unreadable: not a link we can name.
309
+ }
310
+ let dir = path.dirname(path.resolve(filePath));
311
+ for (;;) {
312
+ try {
313
+ if (fs.lstatSync(dir).isSymbolicLink())
314
+ return 'folder';
315
+ }
316
+ catch {
317
+ // Keep walking up.
318
+ }
319
+ const parent = path.dirname(dir);
320
+ if (parent === dir)
321
+ return null;
322
+ dir = parent;
186
323
  }
324
+ }
325
+ function ownerFileNotice(filePath, home) {
326
+ const file = printablePath(filePath);
327
+ // Windows reports no owner-only mode bits, so owner files never pass there.
328
+ if (process.platform === 'win32') {
329
+ return `Ignored the Marrow key in ${file}: owner-only file permissions cannot be confirmed on Windows. Fix: set MARROW_API_KEY in your environment or in the MCP client's env settings.`;
330
+ }
331
+ // The helper also refuses a home directory others can write, so name it too.
332
+ let homeDir = null;
333
+ try {
334
+ const resolvedHome = path.resolve(home);
335
+ if (resolvedHome !== path.dirname(path.resolve(filePath)) && (fs.lstatSync(resolvedHome).mode & 0o022) !== 0) {
336
+ homeDir = resolvedHome;
337
+ }
338
+ }
339
+ catch {
340
+ // Unreadable home: the generic advice stands.
341
+ }
342
+ // chmod follows a symlink and would change its target while the link stays
343
+ // refused, so a symlinked file or folder gets no chmod operand on that path.
344
+ const lead = `Ignored the Marrow key in ${file}: the file is not owner-only (a symlink, readable or writable by others, owned by another user, or in a directory others can write).`;
345
+ const link = symlinkInChain(filePath);
346
+ if (link === 'file') {
347
+ return `${lead} Fix: replace the symlink with the real file (owner-only, mode 600); do not run chmod on the link.`;
348
+ }
349
+ if (link === 'folder') {
350
+ return `${lead} Fix: remove the symlink from the folder chain so the path uses real folders only, then make the file owner-only (mode 600); do not run chmod through the link.`;
351
+ }
352
+ const tail = ' (replace a symlink with the file itself)';
353
+ // Command operands use the real paths, quoted; if any cannot be printed safely, name no path.
354
+ const dirOp = shellOperand(path.dirname(filePath));
355
+ const fileOp = shellOperand(filePath);
356
+ const homeOp = homeDir === null ? '' : shellOperand(homeDir);
357
+ if (fileOp !== null && dirOp !== null && homeOp !== null) {
358
+ const homeFix = homeDir === null ? '' : ` && chmod go-w ${homeOp}`;
359
+ return `${lead} Fix: chmod 600 ${fileOp} && chmod 700 ${dirOp}${homeFix}${tail}.`;
360
+ }
361
+ const homeText = homeDir === null ? '' : ' and chmod go-w on your home folder';
362
+ return `${lead} Fix: run chmod 600 on the file named above and chmod 700 on its folder${homeText}${tail}.`;
363
+ }
364
+ function fromFile(filePath, found, parsed, env) {
365
+ return {
366
+ apiKey: found.key,
367
+ baseUrl: parsed.MARROW_BASE_URL || env.MARROW_BASE_URL || DEFAULT_BASE_URL,
368
+ agentId: parsed.MARROW_FLEET_AGENT_ID || parsed.MARROW_AGENT_ID || env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID,
369
+ sessionId: parsed.MARROW_SESSION_ID || env.MARROW_SESSION_ID,
370
+ source: `${filePath}:${found.source}`,
371
+ missing: false,
372
+ exactFix: `Marrow key was found in ${printablePath(filePath)}. Keep this file private and run npx @getmarrow/install doctor to verify.`,
373
+ };
374
+ }
375
+ function missingKey(env, ignored) {
187
376
  return {
188
377
  apiKey: '',
189
378
  baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
@@ -191,7 +380,8 @@ function resolveMarrowEnv(options = {}) {
191
380
  sessionId: env.MARROW_SESSION_ID,
192
381
  source: null,
193
382
  missing: true,
194
- exactFix: 'Create an API key at https://getmarrow.ai, then export MARROW_API_KEY from trusted secret storage or place it in ~/.marrow/env with owner-only permissions before running npx -y --package=@getmarrow/mcp@latest marrow-mcp setup.',
383
+ exactFix: 'Create an API key at https://getmarrow.ai, then export MARROW_API_KEY from trusted secret storage or place it in ~/.marrow/env with owner-only permissions before running npx -y --package=@getmarrow/mcp@latest marrow-mcp setup.'
384
+ + (ignored ? `\n${ignored}` : ''),
195
385
  };
196
386
  }
197
387
  //# sourceMappingURL=env.js.map