gogcli-mcp 4.2.5 → 4.3.0

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 (57) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +714 -216
  4. package/dist/lib.js +750 -216
  5. package/manifest.json +9 -2
  6. package/package.json +2 -2
  7. package/server.json +2 -2
  8. package/src/arg-guard.ts +68 -0
  9. package/src/argv.ts +27 -0
  10. package/src/attachment-root.ts +111 -0
  11. package/src/attachments.ts +1 -0
  12. package/src/blob-upload.ts +4 -2
  13. package/src/file-roots.ts +66 -0
  14. package/src/gmail-dispatch-guard.ts +56 -16
  15. package/src/gmail-results.ts +21 -2
  16. package/src/lib.ts +6 -0
  17. package/src/run-path-guard.ts +118 -0
  18. package/src/runner.ts +86 -18
  19. package/src/tools/api.ts +39 -7
  20. package/src/tools/appscript.ts +12 -8
  21. package/src/tools/auth.ts +13 -3
  22. package/src/tools/calendar.ts +10 -8
  23. package/src/tools/chat.ts +16 -13
  24. package/src/tools/classroom.ts +27 -25
  25. package/src/tools/contacts.ts +5 -3
  26. package/src/tools/docs.ts +8 -6
  27. package/src/tools/drive.ts +42 -18
  28. package/src/tools/gmail.ts +71 -11
  29. package/src/tools/sheets.ts +10 -8
  30. package/src/tools/slides.ts +14 -9
  31. package/src/tools/tasks.ts +7 -5
  32. package/src/tools/utils.ts +52 -6
  33. package/tests/arg-guard.test.ts +80 -0
  34. package/tests/attachment-root.test.ts +130 -0
  35. package/tests/attachments.test.ts +8 -0
  36. package/tests/blob-upload.test.ts +4 -3
  37. package/tests/file-roots.test.ts +101 -0
  38. package/tests/gmail-dispatch-guard.test.ts +46 -0
  39. package/tests/gmail-results.test.ts +35 -1
  40. package/tests/run-path-guard.test.ts +142 -0
  41. package/tests/runner.test.ts +136 -0
  42. package/tests/tools/api.test.ts +74 -8
  43. package/tests/tools/appscript.test.ts +34 -8
  44. package/tests/tools/auth.test.ts +44 -17
  45. package/tests/tools/calendar.test.ts +29 -27
  46. package/tests/tools/chat.test.ts +46 -20
  47. package/tests/tools/classroom.test.ts +39 -38
  48. package/tests/tools/contacts.test.ts +3 -2
  49. package/tests/tools/docs.test.ts +44 -15
  50. package/tests/tools/drive.test.ts +106 -19
  51. package/tests/tools/gmail.test.ts +227 -29
  52. package/tests/tools/run-tool-examples.test.ts +69 -0
  53. package/tests/tools/sheets.test.ts +16 -15
  54. package/tests/tools/slides.test.ts +47 -11
  55. package/tests/tools/tasks.test.ts +7 -6
  56. package/tests/tools/utils.test.ts +32 -31
  57. package/vitest.config.ts +5 -0
@@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { registerCalendarTools, CALENDAR_EVENTS_COMPACT_FIELDS } from '../../src/tools/calendar.js';
3
3
  import * as runner from '../../src/runner.js';
4
4
  import { createTestHarness } from '@chrischall/mcp-utils/test';
5
+ import { pos } from '../../src/argv.js';
6
+ import type { GogArg } from '../../src/runner.js';
5
7
 
6
8
  vi.mock('../../src/runner.js');
7
9
 
@@ -10,7 +12,7 @@ const setupHandlers = () => createTestHarness(registerCalendarTools);
10
12
  // gog_calendar_events answers in the compact rung by default, so every call
11
13
  // carries the mask. These keep the window-flag tests below about window flags
12
14
  // while still asserting the shipped default rather than an opted-out view.
13
- const evArgs = (...extra: string[]) =>
15
+ const evArgs = (...extra: GogArg[]) =>
14
16
  ['calendar', 'events', ...extra, `--fields=${CALENDAR_EVENTS_COMPACT_FIELDS}`];
15
17
  const evOpts = { account: undefined, fieldsMask: CALENDAR_EVENTS_COMPACT_FIELDS };
16
18
 
@@ -67,7 +69,7 @@ describe('gog_calendar_events', () => {
67
69
  query: 'standup',
68
70
  all: true,
69
71
  });
70
- expect(runner.run).toHaveBeenCalledWith(evArgs('primary', '--from=2026-01-01', '--to=2026-01-31', '--query=standup', '--all'), evOpts);
72
+ expect(runner.run).toHaveBeenCalledWith(evArgs(pos('primary'), '--from=2026-01-01', '--to=2026-01-31', '--query=standup', '--all'), evOpts);
71
73
  });
72
74
 
73
75
  it('appends --today on its own', async () => {
@@ -118,7 +120,7 @@ describe('gog_calendar_get', () => {
118
120
  vi.mocked(runner.run).mockResolvedValue('{"id":"evt1"}');
119
121
  const harness = await setupHandlers();
120
122
  await harness.callTool('gog_calendar_get', { calendarId: 'primary', eventId: 'evt1' });
121
- expect(runner.run).toHaveBeenCalledWith(['calendar', 'event', 'primary', 'evt1'], { account: undefined });
123
+ expect(runner.run).toHaveBeenCalledWith(['calendar', 'event', pos('primary'), pos('evt1')], { account: undefined });
122
124
  });
123
125
 
124
126
  it('appends --timezone when provided', async () => {
@@ -126,7 +128,7 @@ describe('gog_calendar_get', () => {
126
128
  const harness = await setupHandlers();
127
129
  await harness.callTool('gog_calendar_get', { calendarId: 'primary', eventId: 'evt1', timezone: 'local' });
128
130
  expect(runner.run).toHaveBeenCalledWith(
129
- ['calendar', 'event', 'primary', 'evt1', '--timezone=local'],
131
+ ['calendar', 'event', pos('primary'), pos('evt1'), '--timezone=local'],
130
132
  { account: undefined },
131
133
  );
132
134
  });
@@ -150,7 +152,7 @@ describe('gog_calendar_create', () => {
150
152
  to: '2026-04-14T09:30:00Z',
151
153
  });
152
154
  expect(runner.run).toHaveBeenCalledWith(
153
- ['calendar', 'create', 'primary', '--summary=Standup', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z'],
155
+ ['calendar', 'create', pos('primary'), '--summary=Standup', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z'],
154
156
  { account: undefined },
155
157
  );
156
158
  });
@@ -171,7 +173,7 @@ describe('gog_calendar_create', () => {
171
173
  });
172
174
  expect(runner.run).toHaveBeenCalledWith(
173
175
  [
174
- 'calendar', 'create', 'primary',
176
+ 'calendar', 'create', pos('primary'),
175
177
  '--summary=All-day', '--from=2026-04-14', '--to=2026-04-15',
176
178
  '--description=Desc', '--location=NYC', '--attendees=a@b.com,c@d.com', '--all-day',
177
179
  '--timezone=America/New_York',
@@ -195,7 +197,7 @@ describe('gog_calendar_create', () => {
195
197
  });
196
198
  expect(runner.run).toHaveBeenCalledWith(
197
199
  [
198
- 'calendar', 'create', 'primary',
200
+ 'calendar', 'create', pos('primary'),
199
201
  '--summary=Sync', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z',
200
202
  '--with-zoom',
201
203
  ],
@@ -210,7 +212,7 @@ describe('gog_calendar_create', () => {
210
212
  calendarId: 'primary', summary: 's', from: 'f', to: 't', withZoom: false,
211
213
  });
212
214
  expect(runner.run).toHaveBeenCalledWith(
213
- ['calendar', 'create', 'primary', '--summary=s', '--from=f', '--to=t'],
215
+ ['calendar', 'create', pos('primary'), '--summary=s', '--from=f', '--to=t'],
214
216
  { account: undefined },
215
217
  );
216
218
  });
@@ -233,7 +235,7 @@ describe('gog_calendar_update', () => {
233
235
  summary: 'New Title',
234
236
  });
235
237
  expect(runner.run).toHaveBeenCalledWith(
236
- ['calendar', 'update', 'primary', 'evt1', '--summary=New Title'],
238
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--summary=New Title'],
237
239
  { account: undefined },
238
240
  );
239
241
  });
@@ -246,12 +248,12 @@ describe('gog_calendar_update', () => {
246
248
  calendarId: 'primary', eventId: 'evt1', attachments: ['https://drive.google.com/file/d/a', 'https://x.test/b.pdf'],
247
249
  });
248
250
  expect(runner.run).toHaveBeenCalledWith(
249
- ['calendar', 'update', 'primary', 'evt1', '--attachment=https://drive.google.com/file/d/a', '--attachment=https://x.test/b.pdf'],
251
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--attachment=https://drive.google.com/file/d/a', '--attachment=https://x.test/b.pdf'],
250
252
  { account: undefined },
251
253
  );
252
254
  await harness.callTool('gog_calendar_update', { calendarId: 'primary', eventId: 'evt1', attachments: [''] });
253
255
  expect(runner.run).toHaveBeenCalledWith(
254
- ['calendar', 'update', 'primary', 'evt1', '--attachment='],
256
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--attachment='],
255
257
  { account: undefined },
256
258
  );
257
259
  });
@@ -271,7 +273,7 @@ describe('gog_calendar_update', () => {
271
273
  });
272
274
  expect(runner.run).toHaveBeenCalledWith(
273
275
  [
274
- 'calendar', 'update', 'primary', 'evt1',
276
+ 'calendar', 'update', pos('primary'), pos('evt1'),
275
277
  '--summary=New', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T10:00:00Z',
276
278
  '--description=Desc', '--location=NYC', '--attendees=a@b.com',
277
279
  ],
@@ -290,7 +292,7 @@ describe('gog_calendar_update', () => {
290
292
  });
291
293
  expect(runner.run).toHaveBeenCalledWith(
292
294
  [
293
- 'calendar', 'update', 'primary', 'evt1',
295
+ 'calendar', 'update', pos('primary'), pos('evt1'),
294
296
  '--add-attendee=room@resource.calendar.google.com;resource,x@y.com;optional',
295
297
  ],
296
298
  { account: undefined },
@@ -305,7 +307,7 @@ describe('gog_calendar_update', () => {
305
307
  calendarId: 'primary', eventId: 'evt1', withZoom: true,
306
308
  });
307
309
  expect(runner.run).toHaveBeenCalledWith(
308
- ['calendar', 'update', 'primary', 'evt1', '--with-zoom'],
310
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--with-zoom'],
309
311
  { account: undefined },
310
312
  );
311
313
 
@@ -315,7 +317,7 @@ describe('gog_calendar_update', () => {
315
317
  calendarId: 'primary', eventId: 'evt1', regenerateZoom: true,
316
318
  });
317
319
  expect(runner.run).toHaveBeenCalledWith(
318
- ['calendar', 'update', 'primary', 'evt1', '--regenerate-zoom'],
320
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--regenerate-zoom'],
319
321
  { account: undefined },
320
322
  );
321
323
 
@@ -325,7 +327,7 @@ describe('gog_calendar_update', () => {
325
327
  calendarId: 'primary', eventId: 'evt1', removeZoom: true,
326
328
  });
327
329
  expect(runner.run).toHaveBeenCalledWith(
328
- ['calendar', 'update', 'primary', 'evt1', '--remove-zoom'],
330
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--remove-zoom'],
329
331
  { account: undefined },
330
332
  );
331
333
  });
@@ -338,7 +340,7 @@ describe('gog_calendar_update', () => {
338
340
  calendarId: 'primary', eventId: 'evt1', removeMeet: true,
339
341
  });
340
342
  expect(runner.run).toHaveBeenCalledWith(
341
- ['calendar', 'update', 'primary', 'evt1', '--remove-meet'],
343
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--remove-meet'],
342
344
  { account: undefined },
343
345
  );
344
346
  });
@@ -351,7 +353,7 @@ describe('gog_calendar_update', () => {
351
353
  withZoom: false, regenerateZoom: false, removeZoom: false,
352
354
  });
353
355
  expect(runner.run).toHaveBeenCalledWith(
354
- ['calendar', 'update', 'primary', 'evt1'],
356
+ ['calendar', 'update', pos('primary'), pos('evt1')],
355
357
  { account: undefined },
356
358
  );
357
359
  });
@@ -369,7 +371,7 @@ describe('gog_calendar_delete', () => {
369
371
  vi.mocked(runner.run).mockResolvedValue('{}');
370
372
  const harness = await setupHandlers();
371
373
  await harness.callTool('gog_calendar_delete', { calendarId: 'primary', eventId: 'evt1' });
372
- expect(runner.run).toHaveBeenCalledWith(['calendar', 'delete', 'primary', 'evt1', '--force'], { account: undefined });
374
+ expect(runner.run).toHaveBeenCalledWith(['calendar', 'delete', pos('primary'), pos('evt1'), '--force'], { account: undefined });
373
375
  });
374
376
 
375
377
  it('returns error text on failure', async () => {
@@ -386,7 +388,7 @@ describe('gog_calendar_respond', () => {
386
388
  const harness = await setupHandlers();
387
389
  await harness.callTool('gog_calendar_respond', { calendarId: 'primary', eventId: 'evt1', status: 'accepted' });
388
390
  expect(runner.run).toHaveBeenCalledWith(
389
- ['calendar', 'respond', 'primary', 'evt1', '--status=accepted'],
391
+ ['calendar', 'respond', pos('primary'), pos('evt1'), '--status=accepted'],
390
392
  { account: undefined },
391
393
  );
392
394
  });
@@ -401,7 +403,7 @@ describe('gog_calendar_respond', () => {
401
403
  comment: 'Can\'t make it',
402
404
  });
403
405
  expect(runner.run).toHaveBeenCalledWith(
404
- ['calendar', 'respond', 'primary', 'evt1', '--status=declined', '--comment=Can\'t make it'],
406
+ ['calendar', 'respond', pos('primary'), pos('evt1'), '--status=declined', '--comment=Can\'t make it'],
405
407
  { account: undefined },
406
408
  );
407
409
  });
@@ -500,7 +502,7 @@ describe('gog_calendar_create reminders', () => {
500
502
  reminders: ['popup:30m', 'email:1d'],
501
503
  });
502
504
  expect(runner.run).toHaveBeenCalledWith(
503
- ['calendar', 'create', 'primary', '--summary=Standup', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z',
505
+ ['calendar', 'create', pos('primary'), '--summary=Standup', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z',
504
506
  '--reminder=popup:30m', '--reminder=email:1d'],
505
507
  { account: undefined },
506
508
  );
@@ -517,7 +519,7 @@ describe('gog_calendar_create reminders', () => {
517
519
  noReminders: true,
518
520
  });
519
521
  expect(runner.run).toHaveBeenCalledWith(
520
- ['calendar', 'create', 'primary', '--summary=Quiet', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z',
522
+ ['calendar', 'create', pos('primary'), '--summary=Quiet', '--from=2026-04-14T09:00:00Z', '--to=2026-04-14T09:30:00Z',
521
523
  '--no-reminders'],
522
524
  { account: undefined },
523
525
  );
@@ -559,7 +561,7 @@ describe('gog_calendar_update reminders', () => {
559
561
  calendarId: 'primary', eventId: 'evt1', reminders: ['popup:15m'],
560
562
  });
561
563
  expect(runner.run).toHaveBeenCalledWith(
562
- ['calendar', 'update', 'primary', 'evt1', '--reminder=popup:15m'],
564
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--reminder=popup:15m'],
563
565
  { account: undefined },
564
566
  );
565
567
  });
@@ -573,7 +575,7 @@ describe('gog_calendar_update reminders', () => {
573
575
  calendarId: 'primary', eventId: 'evt1', reminders: [],
574
576
  });
575
577
  expect(runner.run).toHaveBeenCalledWith(
576
- ['calendar', 'update', 'primary', 'evt1', '--reminder='],
578
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--reminder='],
577
579
  { account: undefined },
578
580
  );
579
581
  });
@@ -585,7 +587,7 @@ describe('gog_calendar_update reminders', () => {
585
587
  calendarId: 'primary', eventId: 'evt1', noReminders: true,
586
588
  });
587
589
  expect(runner.run).toHaveBeenCalledWith(
588
- ['calendar', 'update', 'primary', 'evt1', '--no-reminders'],
590
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--no-reminders'],
589
591
  { account: undefined },
590
592
  );
591
593
  });
@@ -597,7 +599,7 @@ describe('gog_calendar_update reminders', () => {
597
599
  calendarId: 'primary', eventId: 'evt1', summary: 'Renamed',
598
600
  });
599
601
  expect(runner.run).toHaveBeenCalledWith(
600
- ['calendar', 'update', 'primary', 'evt1', '--summary=Renamed'],
602
+ ['calendar', 'update', pos('primary'), pos('evt1'), '--summary=Renamed'],
601
603
  { account: undefined },
602
604
  );
603
605
  });
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { registerChatTools } from '../../src/tools/chat.js';
3
3
  import * as runner from '../../src/runner.js';
4
4
  import { createTestHarness } from '@chrischall/mcp-utils/test';
5
+ import { pos } from '../../src/argv.js';
5
6
 
6
7
  vi.mock('../../src/runner.js');
7
8
 
@@ -58,7 +59,7 @@ describe('gog_chat_spaces_find', () => {
58
59
  vi.mocked(runner.run).mockResolvedValue('{}');
59
60
  const harness = await setupHandlers();
60
61
  await harness.callTool('gog_chat_spaces_find', { displayName: 'Team' });
61
- expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'find', 'Team'], { account: undefined });
62
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'find', pos('Team')], { account: undefined });
62
63
  });
63
64
 
64
65
  it('passes --exact and --max', async () => {
@@ -66,7 +67,7 @@ describe('gog_chat_spaces_find', () => {
66
67
  const harness = await setupHandlers();
67
68
  await harness.callTool('gog_chat_spaces_find', { displayName: 'Team', exact: true, max: 5 });
68
69
  expect(runner.run).toHaveBeenCalledWith(
69
- ['chat', 'spaces', 'find', 'Team', '--exact', '--max=5'],
70
+ ['chat', 'spaces', 'find', pos('Team'), '--exact', '--max=5'],
70
71
  { account: undefined },
71
72
  );
72
73
  });
@@ -78,7 +79,7 @@ describe('gog_chat_spaces_create', () => {
78
79
  const harness = await setupHandlers();
79
80
  await harness.callTool('gog_chat_spaces_create', { displayName: 'Launch', members: ['a@b.com', 'c@d.com'] });
80
81
  expect(runner.run).toHaveBeenCalledWith(
81
- ['chat', 'spaces', 'create', 'Launch', '--member=a@b.com', '--member=c@d.com'],
82
+ ['chat', 'spaces', 'create', pos('Launch'), '--member=a@b.com', '--member=c@d.com'],
82
83
  { account: undefined },
83
84
  );
84
85
  });
@@ -87,7 +88,7 @@ describe('gog_chat_spaces_create', () => {
87
88
  vi.mocked(runner.run).mockResolvedValue('{}');
88
89
  const harness = await setupHandlers();
89
90
  await harness.callTool('gog_chat_spaces_create', { displayName: 'Solo' });
90
- expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'create', 'Solo'], { account: undefined });
91
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'create', pos('Solo')], { account: undefined });
91
92
  });
92
93
  });
93
94
 
@@ -97,7 +98,7 @@ describe('gog_chat_threads_list', () => {
97
98
  const harness = await setupHandlers();
98
99
  await harness.callTool('gog_chat_threads_list', { space: 'spaces/AAA', max: 10 });
99
100
  expect(runner.run).toHaveBeenCalledWith(
100
- ['chat', 'threads', 'list', 'spaces/AAA', '--max=10'],
101
+ ['chat', 'threads', 'list', pos('spaces/AAA'), '--max=10'],
101
102
  { account: undefined },
102
103
  );
103
104
  });
@@ -108,7 +109,7 @@ describe('gog_chat_messages_list', () => {
108
109
  vi.mocked(runner.run).mockResolvedValue('{}');
109
110
  const harness = await setupHandlers();
110
111
  await harness.callTool('gog_chat_messages_list', { space: 'spaces/AAA' });
111
- expect(runner.run).toHaveBeenCalledWith(['chat', 'messages', 'list', 'spaces/AAA'], { account: undefined });
112
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'messages', 'list', pos('spaces/AAA')], { account: undefined });
112
113
  });
113
114
 
114
115
  it('filters by thread, unread and order', async () => {
@@ -118,7 +119,7 @@ describe('gog_chat_messages_list', () => {
118
119
  space: 'spaces/AAA', thread: 'spaces/AAA/threads/T', unread: true, order: 'createTime desc', max: 50,
119
120
  });
120
121
  expect(runner.run).toHaveBeenCalledWith(
121
- ['chat', 'messages', 'list', 'spaces/AAA', '--thread=spaces/AAA/threads/T', '--unread',
122
+ ['chat', 'messages', 'list', pos('spaces/AAA'), '--thread=spaces/AAA/threads/T', '--unread',
122
123
  '--order=createTime desc', '--max=50'],
123
124
  { account: undefined },
124
125
  );
@@ -138,7 +139,7 @@ describe('gog_chat_messages_search', () => {
138
139
  const harness = await setupHandlers();
139
140
  await harness.callTool('gog_chat_messages_search', { query: 'project decision' });
140
141
  expect(runner.run).toHaveBeenCalledWith(
141
- ['chat', 'messages', 'search', 'project decision'],
142
+ ['chat', 'messages', 'search', pos('project decision')],
142
143
  { account: undefined },
143
144
  );
144
145
  });
@@ -156,7 +157,7 @@ describe('gog_chat_messages_search', () => {
156
157
  all: true,
157
158
  });
158
159
  expect(runner.run).toHaveBeenCalledWith(
159
- ['chat', 'messages', 'search', 'from:alice@example.com budget', '--order=create_time desc',
160
+ ['chat', 'messages', 'search', pos('from:alice@example.com budget'), '--order=create_time desc',
160
161
  '--view=full', '--markup=markdown', '--max=100', '--page=tok', '--all'],
161
162
  { account: undefined },
162
163
  );
@@ -198,7 +199,7 @@ describe('gog_chat_messages_send', () => {
198
199
  const harness = await setupHandlers();
199
200
  await harness.callTool('gog_chat_messages_send', { space: 'spaces/AAA', text: 'hi' });
200
201
  expect(runner.run).toHaveBeenCalledWith(
201
- ['chat', 'messages', 'send', 'spaces/AAA', '--text=hi'],
202
+ ['chat', 'messages', 'send', pos('spaces/AAA'), '--text=hi'],
202
203
  { account: undefined },
203
204
  );
204
205
  });
@@ -210,7 +211,7 @@ describe('gog_chat_messages_send', () => {
210
211
  space: 'spaces/AAA', text: 'see this', thread: 'spaces/AAA/threads/T', attach: ['/tmp/a.png'],
211
212
  });
212
213
  expect(runner.run).toHaveBeenCalledWith(
213
- ['chat', 'messages', 'send', 'spaces/AAA', '--text=see this', '--thread=spaces/AAA/threads/T', '--attach=/tmp/a.png'],
214
+ ['chat', 'messages', 'send', pos('spaces/AAA'), '--text=see this', '--thread=spaces/AAA/threads/T', '--attach=/tmp/a.png'],
214
215
  { account: undefined },
215
216
  );
216
217
  });
@@ -234,7 +235,7 @@ describe('gog_chat_messages_send', () => {
234
235
  const harness = await setupHandlers();
235
236
  await harness.callTool('gog_chat_messages_send', { space: 'spaces/AAA', attach: ['/tmp/a.png'] });
236
237
  expect(runner.run).toHaveBeenCalledWith(
237
- ['chat', 'messages', 'send', 'spaces/AAA', '--attach=/tmp/a.png'],
238
+ ['chat', 'messages', 'send', pos('spaces/AAA'), '--attach=/tmp/a.png'],
238
239
  { account: undefined },
239
240
  );
240
241
  });
@@ -253,7 +254,7 @@ describe('gog_chat_dm_send', () => {
253
254
  const harness = await setupHandlers();
254
255
  await harness.callTool('gog_chat_dm_send', { email: 'a@b.com', text: 'hi' });
255
256
  expect(runner.run).toHaveBeenCalledWith(
256
- ['chat', 'dm', 'send', 'a@b.com', '--text=hi'],
257
+ ['chat', 'dm', 'send', pos('a@b.com'), '--text=hi'],
257
258
  { account: undefined },
258
259
  );
259
260
  });
@@ -263,7 +264,7 @@ describe('gog_chat_dm_send', () => {
263
264
  const harness = await setupHandlers();
264
265
  await harness.callTool('gog_chat_dm_send', { email: 'a@b.com', text: 'hi', thread: 'spaces/D/threads/T' });
265
266
  expect(runner.run).toHaveBeenCalledWith(
266
- ['chat', 'dm', 'send', 'a@b.com', '--text=hi', '--thread=spaces/D/threads/T'],
267
+ ['chat', 'dm', 'send', pos('a@b.com'), '--text=hi', '--thread=spaces/D/threads/T'],
267
268
  { account: undefined },
268
269
  );
269
270
  });
@@ -274,7 +275,7 @@ describe('gog_chat_dm_space', () => {
274
275
  vi.mocked(runner.run).mockResolvedValue('{}');
275
276
  const harness = await setupHandlers();
276
277
  await harness.callTool('gog_chat_dm_space', { email: 'a@b.com' });
277
- expect(runner.run).toHaveBeenCalledWith(['chat', 'dm', 'space', 'a@b.com'], { account: undefined });
278
+ expect(runner.run).toHaveBeenCalledWith(['chat', 'dm', 'space', pos('a@b.com')], { account: undefined });
278
279
  });
279
280
  });
280
281
 
@@ -284,7 +285,7 @@ describe('gog_chat_reactions_list', () => {
284
285
  const harness = await setupHandlers();
285
286
  await harness.callTool('gog_chat_reactions_list', { message: 'MSG', space: 'spaces/AAA', max: 10 });
286
287
  expect(runner.run).toHaveBeenCalledWith(
287
- ['chat', 'messages', 'reactions', 'list', 'MSG', '--space=spaces/AAA', '--max=10'],
288
+ ['chat', 'messages', 'reactions', 'list', pos('MSG'), '--space=spaces/AAA', '--max=10'],
288
289
  { account: undefined },
289
290
  );
290
291
  });
@@ -294,7 +295,7 @@ describe('gog_chat_reactions_list', () => {
294
295
  const harness = await setupHandlers();
295
296
  await harness.callTool('gog_chat_reactions_list', { message: 'spaces/AAA/messages/M' });
296
297
  expect(runner.run).toHaveBeenCalledWith(
297
- ['chat', 'messages', 'reactions', 'list', 'spaces/AAA/messages/M'],
298
+ ['chat', 'messages', 'reactions', 'list', pos('spaces/AAA/messages/M')],
298
299
  { account: undefined },
299
300
  );
300
301
  });
@@ -306,7 +307,7 @@ describe('gog_chat_reactions_create', () => {
306
307
  const harness = await setupHandlers();
307
308
  await harness.callTool('gog_chat_reactions_create', { message: 'spaces/AAA/messages/M', emoji: '👍' });
308
309
  expect(runner.run).toHaveBeenCalledWith(
309
- ['chat', 'messages', 'reactions', 'create', 'spaces/AAA/messages/M', '👍'],
310
+ ['chat', 'messages', 'reactions', 'create', pos('spaces/AAA/messages/M'), pos('👍')],
310
311
  { account: undefined },
311
312
  );
312
313
  });
@@ -316,7 +317,7 @@ describe('gog_chat_reactions_create', () => {
316
317
  const harness = await setupHandlers();
317
318
  await harness.callTool('gog_chat_reactions_create', { message: 'M', emoji: '👍', space: 'spaces/AAA' });
318
319
  expect(runner.run).toHaveBeenCalledWith(
319
- ['chat', 'messages', 'reactions', 'create', 'M', '👍', '--space=spaces/AAA'],
320
+ ['chat', 'messages', 'reactions', 'create', pos('M'), pos('👍'), '--space=spaces/AAA'],
320
321
  { account: undefined },
321
322
  );
322
323
  });
@@ -328,7 +329,7 @@ describe('gog_chat_reactions_delete', () => {
328
329
  const harness = await setupHandlers();
329
330
  await harness.callTool('gog_chat_reactions_delete', { reaction: 'spaces/AAA/messages/M/reactions/R' });
330
331
  expect(runner.run).toHaveBeenCalledWith(
331
- ['chat', 'messages', 'reactions', 'delete', 'spaces/AAA/messages/M/reactions/R'],
332
+ ['chat', 'messages', 'reactions', 'delete', pos('spaces/AAA/messages/M/reactions/R')],
332
333
  { account: undefined },
333
334
  );
334
335
  });
@@ -342,3 +343,28 @@ describe('gog_chat_run', () => {
342
343
  expect(runner.run).toHaveBeenCalledWith(['chat', 'spaces', 'list'], { account: undefined });
343
344
  });
344
345
  });
346
+
347
+ // SEC-3/SEC-4: every model-supplied server path must resolve inside an
348
+ // operator-configured root (GOG_FILE_ROOTS). The suite runs with '/' so the
349
+ // arg-shape tests can use any path; these narrow it.
350
+ async function withFileRoots<T>(roots: string, fn: () => Promise<T>): Promise<T> {
351
+ const prev = process.env.GOG_FILE_ROOTS;
352
+ process.env.GOG_FILE_ROOTS = roots;
353
+ try {
354
+ return await fn();
355
+ } finally {
356
+ process.env.GOG_FILE_ROOTS = prev;
357
+ }
358
+ }
359
+
360
+ describe('gog_chat_messages_send — server paths', () => {
361
+ it('refuses an attach path outside GOG_FILE_ROOTS', async () => {
362
+ const harness = await setupHandlers();
363
+ const result = await withFileRoots('/srv/gog-files', () => harness.callTool('gog_chat_messages_send', {
364
+ space: 'spaces/AAA', attach: ['/etc/passwd'],
365
+ }));
366
+ expect(result.isError).toBe(true);
367
+ expect(result.content[0].text).toContain('attach "/etc/passwd" is outside');
368
+ expect(runner.run).not.toHaveBeenCalled();
369
+ });
370
+ });