@luckydraw/cumulus 1.0.2 → 1.0.4

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.
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Tool inventory + allowlist compilation (task 137).
3
+ *
4
+ * WHY THIS EXISTS
5
+ * A thread that should only be able to do a few things (a public web-app visitor
6
+ * panel) previously had to be configured with a DENYLIST — @cdda hand-enumerated 47
7
+ * tool names to get a safe surface. That inverts maintenance onto every app author:
8
+ * any tool cumulus or the Claude CLI adds in a future release is silently granted to
9
+ * every visitor on every deployed app until each author notices and edits their list.
10
+ *
11
+ * The fix is an allowlist. But `--allowedTools` on the Claude CLI is an AUTO-APPROVE
12
+ * list, not a restriction — measured under three permission modes, `--allowedTools Glob`
13
+ * failed to block Bash every time, and cumulus spawns with `--permission-mode
14
+ * bypassPermissions` where an auto-approve list is a guaranteed no-op. `--disallowedTools`
15
+ * DOES restrict there (also measured). So an allowlist has to be COMPILED into a denylist:
16
+ *
17
+ * denylist = (everything that exists) − (what this thread may use)
18
+ *
19
+ * which is why this module has to know the full inventory.
20
+ *
21
+ * WHERE THE INVENTORY COMES FROM (three sources, because no single one is complete)
22
+ * 1. Built-in CLI tools — read from the `system:init` stream event, which carries the
23
+ * CLI's own `tools[]`. Probed once per process (see probeBuiltinTools).
24
+ * 2. cumulus's own MCP tools — derived from the definitions cumulus itself ships.
25
+ * 3. Namespace `extraMcpServers` — NOT knowable here, and deliberately not closed over.
26
+ * Those are the app author's own shim tools, granted to that namespace on purpose.
27
+ *
28
+ * The seed list is a FLOOR, not merely a fallback: the inventory is always the union of
29
+ * the probe and the seed, so a probe that returns a short list (different cwd, different
30
+ * plugin set, a future CLI that changes the event shape) can never quietly widen what a
31
+ * constrained thread is allowed to reach.
32
+ */
33
+ import { spawn } from 'child_process';
34
+ import * as os from 'os';
35
+ import { getToolDefinitions } from '../mcp/tool-handler.js';
36
+ /**
37
+ * Built-in Claude CLI tools, as reported by `system:init` on CLI 2.1.217.
38
+ *
39
+ * This is a FLOOR for the inventory, not a fallback — see the module comment. Adding a
40
+ * name here can only ever deny more on an allowlisted thread; it has no effect on a
41
+ * thread without `allowedTools`. Safe to update from a probe of a newer CLI.
42
+ */
43
+ export const SEED_BUILTIN_TOOLS = [
44
+ 'Bash',
45
+ 'CronCreate',
46
+ 'CronDelete',
47
+ 'CronList',
48
+ 'DesignSync',
49
+ 'Edit',
50
+ 'EnterWorktree',
51
+ 'ExitWorktree',
52
+ 'Monitor',
53
+ 'NotebookEdit',
54
+ 'PushNotification',
55
+ 'Read',
56
+ 'RemoteTrigger',
57
+ 'ReportFindings',
58
+ 'ScheduleWakeup',
59
+ 'SendMessage',
60
+ 'Skill',
61
+ 'Task',
62
+ 'TaskCreate',
63
+ 'TaskGet',
64
+ 'TaskList',
65
+ 'TaskOutput',
66
+ 'TaskStop',
67
+ 'TaskUpdate',
68
+ 'ToolSearch',
69
+ 'WebFetch',
70
+ 'WebSearch',
71
+ 'Workflow',
72
+ 'Write',
73
+ ];
74
+ /**
75
+ * Tools served by the `gateway-agents` MCP server.
76
+ *
77
+ * Duplicated here rather than imported because `gateway-agents-mcp.ts` calls `main()`
78
+ * at top level — it is a stdio binary, and importing it would start a server. A source
79
+ * tripwire test asserts this list matches the tool definitions in that file, so the two
80
+ * cannot drift (same shape as task 132's artifact tripwire).
81
+ *
82
+ * `broadcast` is absent on purpose: it is deliberately not served (task 135).
83
+ */
84
+ export const GATEWAY_AGENTS_TOOL_NAMES = [
85
+ 'cancel_schedule',
86
+ 'create_plastic_app',
87
+ 'list_agents',
88
+ 'list_emails',
89
+ 'list_schedules',
90
+ 'notify_user',
91
+ 'schedule_trigger',
92
+ 'send_email',
93
+ 'send_to_agent',
94
+ 'update_pipeline',
95
+ 'upload_media',
96
+ ];
97
+ /** MCP server keys cumulus attaches itself (see generateMcpConfig). */
98
+ const CUMULUS_HISTORY_SERVER = 'cumulus-history';
99
+ const GATEWAY_AGENTS_SERVER = 'gateway-agents';
100
+ /** How long to wait for the CLI to emit its init event before giving up on the probe. */
101
+ const PROBE_TIMEOUT_MS = 15_000;
102
+ /** Fully-qualified names of the MCP tools cumulus ships itself. */
103
+ export function cumulusMcpToolNames() {
104
+ const history = getToolDefinitions().map(t => `mcp__${CUMULUS_HISTORY_SERVER}__${t.name}`);
105
+ const agents = GATEWAY_AGENTS_TOOL_NAMES.map(n => `mcp__${GATEWAY_AGENTS_SERVER}__${n}`);
106
+ return [...history, ...agents];
107
+ }
108
+ /**
109
+ * Ask the Claude CLI what built-in tools it has, by reading the `tools[]` array off its
110
+ * `system:init` stream event and killing the subprocess immediately.
111
+ *
112
+ * Measured cost: init arrives ~871ms in and is the ONLY event before the kill — no
113
+ * assistant output, no result event, so nothing comes back from the model. A prompt is
114
+ * required: with stdin closed the CLI exits before emitting init (0 lines), and merely
115
+ * holding stdin open emits nothing at all (20s). Hence the one-character `x`.
116
+ *
117
+ * Runs in a neutral cwd so a project's own settings cannot shape the answer, and the
118
+ * result is unioned with the seed floor by the caller either way.
119
+ *
120
+ * Rejects on timeout, spawn failure, or a missing/empty `tools[]`. Never throws
121
+ * synchronously; the caller treats any rejection as "use the floor".
122
+ *
123
+ * `claudePath` is supplied by the caller, which already has it resolved. Taking it as a
124
+ * parameter rather than importing `resolveClaudeCli` avoids a module cycle: that function
125
+ * lives in gateway.ts, which imports this module.
126
+ */
127
+ export function probeBuiltinTools(claudePath) {
128
+ return new Promise((resolve, reject) => {
129
+ const bin = claudePath || 'claude';
130
+ // Same env hygiene as the real spawn: CLAUDE* vars would leak the parent session.
131
+ const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CLAUDE') && key !== 'CLAUDECODE'));
132
+ let child;
133
+ try {
134
+ child = spawn(bin, [
135
+ '--print',
136
+ '--output-format',
137
+ 'stream-json',
138
+ '--verbose',
139
+ '--permission-mode',
140
+ 'bypassPermissions',
141
+ 'x',
142
+ ], { stdio: ['ignore', 'pipe', 'ignore'], cwd: os.tmpdir(), env: cleanEnv });
143
+ }
144
+ catch (err) {
145
+ reject(err instanceof Error ? err : new Error(String(err)));
146
+ return;
147
+ }
148
+ let settled = false;
149
+ let buf = '';
150
+ const finish = (fn) => {
151
+ if (settled)
152
+ return;
153
+ settled = true;
154
+ clearTimeout(timer);
155
+ try {
156
+ child.kill('SIGKILL');
157
+ }
158
+ catch {
159
+ // already gone
160
+ }
161
+ fn();
162
+ };
163
+ const timer = setTimeout(() => finish(() => reject(new Error('tool inventory probe timed out'))), PROBE_TIMEOUT_MS);
164
+ // Don't hold the event loop open on a probe nobody is waiting for.
165
+ timer.unref?.();
166
+ child.stdout?.on('data', (chunk) => {
167
+ buf += chunk.toString();
168
+ let nl;
169
+ while ((nl = buf.indexOf('\n')) >= 0) {
170
+ const line = buf.slice(0, nl);
171
+ buf = buf.slice(nl + 1);
172
+ if (!line.trim())
173
+ continue;
174
+ let parsed;
175
+ try {
176
+ parsed = JSON.parse(line);
177
+ }
178
+ catch {
179
+ continue;
180
+ }
181
+ if (parsed.type !== 'system' || parsed.subtype !== 'init')
182
+ continue;
183
+ const tools = Array.isArray(parsed.tools)
184
+ ? parsed.tools.filter((t) => typeof t === 'string')
185
+ : [];
186
+ if (tools.length === 0) {
187
+ finish(() => reject(new Error('init event carried no tools')));
188
+ return;
189
+ }
190
+ finish(() => resolve(tools));
191
+ return;
192
+ }
193
+ });
194
+ child.on('error', err => finish(() => reject(err)));
195
+ // 'close', not 'exit': close fires after stdio has drained, so a CLI that prints init
196
+ // and exits immediately still resolves. On 'exit' the reject could win the race.
197
+ child.on('close', () => finish(() => reject(new Error('CLI exited before emitting init'))));
198
+ });
199
+ }
200
+ /** Memoized so the probe runs at most once per gateway process. */
201
+ let inventoryPromise = null;
202
+ /**
203
+ * The full set of tool names a spawned turn could otherwise reach.
204
+ *
205
+ * Deliberately NOT threaded through `MessagePipelineOptions`: task 113 is the recorded
206
+ * case of a field added to one of four spawn paths and silently doing nothing in the
207
+ * other three, and here a miss would fail OPEN. Module-scope memoization means the single
208
+ * args-assembly site is the only consumer and it cannot be half-wired.
209
+ */
210
+ export function getToolInventory(claudePath) {
211
+ if (!inventoryPromise) {
212
+ inventoryPromise = probeBuiltinTools(claudePath)
213
+ .catch(err => {
214
+ console.warn(`[Gateway] Tool inventory probe failed (${err instanceof Error ? err.message : err}); ` +
215
+ 'using the seed floor. Allowlisted threads stay closed.');
216
+ return [];
217
+ })
218
+ .then(probed => {
219
+ const all = new Set([...SEED_BUILTIN_TOOLS, ...probed, ...cumulusMcpToolNames()]);
220
+ return [...all].sort();
221
+ });
222
+ }
223
+ return inventoryPromise;
224
+ }
225
+ /** Test seam: drop the memo so a suite can probe with a stub. */
226
+ export function resetToolInventoryCache() {
227
+ inventoryPromise = null;
228
+ }
229
+ /** The bare tool name of an `mcp__server__tool` entry, or the name itself. */
230
+ export function bareToolName(name) {
231
+ if (!name.startsWith('mcp__'))
232
+ return name;
233
+ const parts = name.split('__');
234
+ return parts.length > 2 ? parts.slice(2).join('__') : name;
235
+ }
236
+ /**
237
+ * Compile an allowlist into the denylist the CLI actually honours.
238
+ *
239
+ * Returns `existingDisallowed` UNCHANGED when no allowlist is configured — additive, so
240
+ * every thread that does not opt in behaves exactly as before.
241
+ *
242
+ * An allowlist entry matches an inventory entry exactly, or as its bare name, so
243
+ * `read_file` allows `mcp__cumulus-history__read_file` (the system prompt calls it
244
+ * `read_file`; a user should not have to know the prefix). An entry that matches nothing
245
+ * is inert — and inertness DENIES, so a typo fails closed.
246
+ *
247
+ * `disallowedTools` still applies on top: it can subtract from an allowlist but never add.
248
+ */
249
+ export function compileDisallowedTools(allowedTools, inventory, existingDisallowed) {
250
+ if (!allowedTools)
251
+ return existingDisallowed;
252
+ const allowed = new Set(allowedTools);
253
+ const denied = new Set(existingDisallowed ?? []);
254
+ for (const name of inventory) {
255
+ if (allowed.has(name) || allowed.has(bareToolName(name)))
256
+ continue;
257
+ denied.add(name);
258
+ }
259
+ return denied.size ? [...denied].sort() : undefined;
260
+ }
261
+ /**
262
+ * Is `name` (a bare tool name) reachable on this thread?
263
+ *
264
+ * Deliberately inventory-INDEPENDENT: it reads the thread's stated intent, so it gives the
265
+ * same answer on the Claude CLI path and the direct-provider path, whose real inventories
266
+ * differ. Available iff the allowlist admits it (or there is no allowlist) and the denylist
267
+ * does not name it.
268
+ */
269
+ export function toolAvailable(name, policy) {
270
+ const denied = (policy.disallowedTools ?? []).some(d => d === name || bareToolName(d) === name);
271
+ if (denied)
272
+ return false;
273
+ if (!policy.allowedTools)
274
+ return true;
275
+ return policy.allowedTools.some(a => a === name || bareToolName(a) === name);
276
+ }
277
+ export function promptCapabilities(policy) {
278
+ return {
279
+ backgroundWork: toolAvailable('Bash', policy),
280
+ scheduling: toolAvailable('schedule_trigger', policy),
281
+ interAgent: toolAvailable('send_to_agent', policy),
282
+ };
283
+ }
284
+ /** Section header prefixes, keyed by the capability that keeps them. */
285
+ const GATED_SECTIONS = [
286
+ ['backgroundWork', 'BACKGROUND WORK:'],
287
+ ['scheduling', 'SCHEDULING'],
288
+ ['interAgent', 'INTER-AGENT MESSAGING:'],
289
+ ];
290
+ /**
291
+ * Remove prompt sections whose tools this thread cannot reach.
292
+ *
293
+ * Sections in SYSTEM_PROMPT_TEMPLATE are blank-line-delimited blocks whose first line is a
294
+ * header, so this is a split/filter/join — lossless when nothing is gated (regression-locked),
295
+ * and a no-op on a custom template that has no such headers.
296
+ *
297
+ * Only ever REMOVES text, so an unconstrained thread's prompt is byte-identical and the task
298
+ * 117 static-prompt budget cannot be breached by this function.
299
+ */
300
+ export function applyCapabilityGates(template, caps) {
301
+ const dropPrefixes = GATED_SECTIONS.filter(([cap]) => !caps[cap]).map(([, prefix]) => prefix);
302
+ if (dropPrefixes.length === 0)
303
+ return template;
304
+ return template
305
+ .split('\n\n')
306
+ .filter(block => !dropPrefixes.some(prefix => block.startsWith(prefix)))
307
+ .join('\n\n');
308
+ }
309
+ //# sourceMappingURL=tool-inventory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-inventory.js","sourceRoot":"","sources":["../../src/lib/tool-inventory.ts"],"names":[],"mappings":"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"}
package/dist/mcp/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import * as path from 'path';
2
3
  import { runServer } from './server.js';
3
4
  const threadPath = process.env['CUMULUS_THREAD_PATH'];
4
5
  const contentPath = process.env['CUMULUS_CONTENT_PATH'];
5
6
  const sessionsPath = process.env['CUMULUS_SESSIONS_PATH'];
6
7
  const sessionId = process.env['CUMULUS_SESSION_ID'];
8
+ // Task 136 — read_file root confinement, path.delimiter-separated. Absent = unrestricted.
9
+ const readFileRoots = process.env['CUMULUS_READ_FILE_ROOTS']?.split(path.delimiter).filter(Boolean);
7
10
  if (!threadPath) {
8
11
  console.error('Error: CUMULUS_THREAD_PATH environment variable is required');
9
12
  console.error('Usage: CUMULUS_THREAD_PATH=/path/to/thread.jsonl node dist/mcp/index.js');
10
13
  process.exit(1);
11
14
  }
12
- runServer(threadPath, contentPath, sessionsPath, sessionId).catch(error => {
15
+ runServer(threadPath, contentPath, sessionsPath, sessionId, readFileRoots).catch(error => {
13
16
  console.error('MCP server error:', error);
14
17
  process.exit(1);
15
18
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AACtD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AACxD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;AAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAEpD,IAAI,CAAC,UAAU,EAAE,CAAC;IAChB,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAC7E,OAAO,CAAC,KAAK,CAAC,yEAAyE,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACxE,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AAEA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AACtD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;AACxD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;AAC1D,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AACpD,0FAA0F;AAC1F,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAEpG,IAAI,CAAC,UAAU,EAAE,CAAC;IAChB,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAC7E,OAAO,CAAC,KAAK,CAAC,yEAAyE,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,CAAC,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACvF,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { ContentStore } from '../lib/content-store.js';
3
3
  import { HistoryStore } from '../lib/history.js';
4
- export declare function createMcpServer(historyStore: HistoryStore, contentStore?: ContentStore, sessionsPath?: string, currentSessionId?: string): Server;
5
- export declare function runServer(threadPath: string, contentPath?: string, sessionsPath?: string, sessionId?: string): Promise<void>;
4
+ export declare function createMcpServer(historyStore: HistoryStore, contentStore?: ContentStore, sessionsPath?: string, currentSessionId?: string, readFileRoots?: string[]): Server;
5
+ export declare function runServer(threadPath: string, contentPath?: string, sessionsPath?: string, sessionId?: string, readFileRoots?: string[]): Promise<void>;
6
6
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,wBAAgB,eAAe,CAC7B,YAAY,EAAE,YAAY,EAC1B,YAAY,CAAC,EAAE,YAAY,EAC3B,YAAY,CAAC,EAAE,MAAM,EACrB,gBAAgB,CAAC,EAAE,MAAM,GACxB,MAAM,CAkBR;AAED,wBAAsB,SAAS,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD,wBAAgB,eAAe,CAC7B,YAAY,EAAE,YAAY,EAC1B,YAAY,CAAC,EAAE,YAAY,EAC3B,YAAY,CAAC,EAAE,MAAM,EACrB,gBAAgB,CAAC,EAAE,MAAM,EACzB,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAwBR;AAED,wBAAsB,SAAS,CAC7B,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,MAAM,EAClB,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,OAAO,CAAC,IAAI,CAAC,CAef"}
@@ -4,8 +4,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
4
4
  import { ContentStore } from '../lib/content-store.js';
5
5
  import { HistoryStore } from '../lib/history.js';
6
6
  import { getToolDefinitions, handleToolCall } from './tool-handler.js';
7
- export function createMcpServer(historyStore, contentStore, sessionsPath, currentSessionId) {
8
- const ctx = { historyStore, contentStore, sessionsPath, currentSessionId };
7
+ export function createMcpServer(historyStore, contentStore, sessionsPath, currentSessionId, readFileRoots) {
8
+ const ctx = {
9
+ historyStore,
10
+ contentStore,
11
+ sessionsPath,
12
+ currentSessionId,
13
+ readFileRoots,
14
+ };
9
15
  const server = new Server({ name: 'cumulus-history', version: '1.0.0' }, { capabilities: { tools: {} } });
10
16
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
11
17
  tools: getToolDefinitions(),
@@ -16,10 +22,10 @@ export function createMcpServer(historyStore, contentStore, sessionsPath, curren
16
22
  });
17
23
  return server;
18
24
  }
19
- export async function runServer(threadPath, contentPath, sessionsPath, sessionId) {
25
+ export async function runServer(threadPath, contentPath, sessionsPath, sessionId, readFileRoots) {
20
26
  const historyStore = new HistoryStore(threadPath);
21
27
  const contentStore = contentPath ? new ContentStore(contentPath) : undefined;
22
- const server = createMcpServer(historyStore, contentStore, sessionsPath, sessionId);
28
+ const server = createMcpServer(historyStore, contentStore, sessionsPath, sessionId, readFileRoots);
23
29
  const transport = new StdioServerTransport();
24
30
  await server.connect(transport);
25
31
  await new Promise(resolve => {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAe,MAAM,mBAAmB,CAAC;AAEpF,MAAM,UAAU,eAAe,CAC7B,YAA0B,EAC1B,YAA2B,EAC3B,YAAqB,EACrB,gBAAyB;IAEzB,MAAM,GAAG,GAAgB,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,CAAC;IAExF,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,EAC7C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,kBAAkB,EAAE;KAC5B,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAC9D,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACjD,OAAO,cAAc,CAAC,IAAI,EAAE,IAA2C,EAAE,GAAG,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,UAAkB,EAClB,WAAoB,EACpB,YAAqB,EACrB,SAAkB;IAElB,MAAM,YAAY,GAAG,IAAI,YAAY,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7E,MAAM,MAAM,GAAG,eAAe,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;IACpF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;QAChC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAe,MAAM,mBAAmB,CAAC;AAEpF,MAAM,UAAU,eAAe,CAC7B,YAA0B,EAC1B,YAA2B,EAC3B,YAAqB,EACrB,gBAAyB,EACzB,aAAwB;IAExB,MAAM,GAAG,GAAgB;QACvB,YAAY;QACZ,YAAY;QACZ,YAAY;QACZ,gBAAgB;QAChB,aAAa;KACd,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,EAC7C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE,kBAAkB,EAAE;KAC5B,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;QAC9D,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACjD,OAAO,cAAc,CAAC,IAAI,EAAE,IAA2C,EAAE,GAAG,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,UAAkB,EAClB,WAAoB,EACpB,YAAqB,EACrB,SAAkB,EAClB,aAAwB;IAExB,MAAM,YAAY,GAAG,IAAI,YAAY,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7E,MAAM,MAAM,GAAG,eAAe,CAC5B,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,aAAa,CACd,CAAC;IACF,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;QAChC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -6,7 +6,18 @@ export interface ToolContext {
6
6
  contentStore?: ContentStore;
7
7
  sessionsPath?: string;
8
8
  currentSessionId?: string;
9
+ /**
10
+ * Directories `read_file` is confined to (task 136). Undefined = unrestricted.
11
+ * Computed by readFileRootsForThread() in lib/gateway.ts; enforced here so a
12
+ * future wiring site cannot forget the check.
13
+ */
14
+ readFileRoots?: string[];
9
15
  }
16
+ /**
17
+ * Is `target` inside one of `roots`? Segment-aware: `/a/bc` is NOT inside `/a/b`.
18
+ * Both sides are resolved first, so `..` traversal cannot escape.
19
+ */
20
+ export declare function isPathWithinRoots(target: string, roots: string[]): boolean;
10
21
  export interface ToolResult {
11
22
  [key: string]: unknown;
12
23
  content: Array<{
@@ -1 +1 @@
1
- {"version":3,"file":"tool-handler.d.ts","sourceRoot":"","sources":["../../src/mcp/tool-handler.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE1D,OAAO,EAAqB,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAQ1E,OAAO,EAAE,YAAY,EAAW,MAAM,mBAAmB,CAAC;AAoM1D,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,IAAI,EAAE,CAmS3C;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,UAAU,CAAC,CA0pCrB"}
1
+ {"version":3,"file":"tool-handler.d.ts","sourceRoot":"","sources":["../../src/mcp/tool-handler.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE1D,OAAO,EAAqB,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAQ1E,OAAO,EAAE,YAAY,EAAW,MAAM,mBAAmB,CAAC;AAoM1D,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,YAAY,CAAC;IAC3B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAM1E;AAED,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,IAAI,EAAE,CAmS3C;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,GAAG,EAAE,WAAW,GACf,OAAO,CAAC,UAAU,CAAC,CA2qCrB"}
@@ -130,6 +130,17 @@ function messageToResult(msg, index) {
130
130
  }
131
131
  return result;
132
132
  }
133
+ /**
134
+ * Is `target` inside one of `roots`? Segment-aware: `/a/bc` is NOT inside `/a/b`.
135
+ * Both sides are resolved first, so `..` traversal cannot escape.
136
+ */
137
+ export function isPathWithinRoots(target, roots) {
138
+ const resolved = path.resolve(target);
139
+ return roots.some(root => {
140
+ const r = path.resolve(root);
141
+ return resolved === r || resolved.startsWith(r.endsWith(path.sep) ? r : r + path.sep);
142
+ });
143
+ }
133
144
  export function getToolDefinitions() {
134
145
  return [
135
146
  {
@@ -401,7 +412,7 @@ export function getToolDefinitions() {
401
412
  ];
402
413
  }
403
414
  export async function handleToolCall(name, args, ctx) {
404
- const { historyStore, contentStore, sessionsPath, currentSessionId } = ctx;
415
+ const { historyStore, contentStore, sessionsPath, currentSessionId, readFileRoots } = ctx;
405
416
  try {
406
417
  switch (name) {
407
418
  case 'search_history': {
@@ -997,8 +1008,23 @@ export async function handleToolCall(name, args, ctx) {
997
1008
  isError: true,
998
1009
  };
999
1010
  }
1000
- // Resolve and validate path
1001
1011
  const resolvedPath = path.resolve(filePath);
1012
+ // Root confinement (task 136) — a thread that denies the built-in Read
1013
+ // tool must not get an unrestricted read_file under a different name.
1014
+ if (readFileRoots?.length && !isPathWithinRoots(resolvedPath, readFileRoots)) {
1015
+ return {
1016
+ content: [
1017
+ {
1018
+ type: 'text',
1019
+ text: JSON.stringify({
1020
+ error: `Path is outside this thread's allowed directories: ${resolvedPath}. ` +
1021
+ `Allowed: ${readFileRoots.join(', ')}`,
1022
+ }),
1023
+ },
1024
+ ],
1025
+ isError: true,
1026
+ };
1027
+ }
1002
1028
  try {
1003
1029
  const stat = await fs.stat(resolvedPath);
1004
1030
  if (stat.isDirectory()) {