gogcli-mcp 4.4.0 → 4.5.1
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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +827 -315
- package/dist/lib.js +832 -281
- package/manifest.json +7 -7
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +102 -115
- package/src/gmail-dispatch-guard.ts +28 -21
- package/src/lib.ts +6 -2
- package/src/send-confirm-token.ts +17 -155
- package/src/tools/api.ts +74 -8
- package/src/tools/appscript.ts +63 -2
- package/src/tools/calendar.ts +83 -13
- package/src/tools/chat.ts +35 -4
- package/src/tools/classroom.ts +254 -25
- package/src/tools/docs.ts +7 -1
- package/src/tools/drive.ts +82 -10
- package/src/tools/gmail.ts +37 -1
- package/src/tools/sheets.ts +16 -1
- package/src/tools/utils.ts +10 -4
- package/tests/gmail-dispatch-guard.test.ts +60 -25
- package/tests/send-confirm-token.test.ts +17 -197
- package/tests/tools/api.test.ts +130 -5
- package/tests/tools/appscript.test.ts +6 -2
- package/tests/tools/dispatch-gates-more.test.ts +447 -0
- package/tests/tools/dispatch-gates.test.ts +74 -13
- package/tests/tools/docs.test.ts +5 -6
- package/tests/tools/drive.test.ts +2 -5
- package/tests/tools/gmail-confirm-token.test.ts +13 -11
- package/tests/tools/run-vets-more.test.ts +269 -0
- package/tests/tools/run-vets.test.ts +158 -4
- package/tests/tools/sheets.test.ts +10 -11
- package/tests/tools/utils.test.ts +38 -41
|
@@ -1,167 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
1
|
+
import { createSpentTokenStore, type SpentTokenStore } from '@chrischall/mcp-utils';
|
|
3
2
|
|
|
4
3
|
// ============================================================================
|
|
5
|
-
// THE TOKEN FALLBACK
|
|
4
|
+
// THE TOKEN FALLBACK's gogcli-mcp half, which is now only its spent-token store.
|
|
5
|
+
// Everything else is the fleet's shared layer in @chrischall/mcp-utils
|
|
6
|
+
// (`confirmationFromEnv` + `requireConfirmationWithFallback`), configured by the
|
|
7
|
+
// same three variables as every other fleet server:
|
|
6
8
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// returns the full preview plus a token and sends nothing; phase 2 presents the
|
|
11
|
-
// token, the tool RE-READS what it would send, and dispatches only if that still
|
|
12
|
-
// hashes to what the token was issued for.
|
|
9
|
+
// MCP_CONFIRM_MODE ask-user (default) | auto | refuse
|
|
10
|
+
// MCP_CONFIRM_TTL_SECONDS token lifetime, default 600
|
|
11
|
+
// MCP_CONFIRM_SECRET HMAC key; default random per process
|
|
13
12
|
//
|
|
14
|
-
// WHAT THIS DOES AND DOES NOT PROVE
|
|
15
|
-
// tool argument, so the gate is the model honouring
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// messageId rotates), a changed recipient or body, or a swapped attachment
|
|
19
|
-
// between the two calls all refuse. Caller-supplied files are bound by content
|
|
20
|
-
// hash; a stored draft's attachments by name and size, since Gmail's
|
|
21
|
-
// attachment ids are unstable and any edit rotates the messageId anyway. One
|
|
22
|
-
// approval acts once, for one tool, account and target, within the TTL — per
|
|
23
|
-
// PROCESS: spent tokens live in memory, so with a shared GOG_CONFIRM_SECRET a
|
|
24
|
-
// restart or a second instance would accept a spent token again until it
|
|
25
|
-
// expires. That is why the mode is opt-in.
|
|
13
|
+
// WHAT THIS DOES AND DOES NOT PROVE: see mcp-utils' confirm-token module. The
|
|
14
|
+
// approval is a tool argument, so under ask-user the gate is the model honouring
|
|
15
|
+
// "show this to the user and wait"; the token guarantees what happens matches
|
|
16
|
+
// what was previewed, once, for one tool, account and target.
|
|
26
17
|
// ============================================================================
|
|
27
18
|
|
|
28
|
-
|
|
19
|
+
const spent = createSpentTokenStore();
|
|
29
20
|
|
|
30
|
-
/**
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
account: string;
|
|
34
|
-
/** The draftId / messageId / query the dispatch acts on. */
|
|
35
|
-
target: string;
|
|
36
|
-
/** A version of the target that rotates on edit — a draft's messageId. */
|
|
37
|
-
revision?: string;
|
|
38
|
-
/** {@link hashSendPayload} of the canonical send payload. */
|
|
39
|
-
payloadHash: string;
|
|
21
|
+
/** This server's spent-token store (one per process). */
|
|
22
|
+
export function confirmSpentStore(): SpentTokenStore {
|
|
23
|
+
return spent;
|
|
40
24
|
}
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
| { ok: true }
|
|
44
|
-
| { ok: false; error: ConfirmTokenError; reason?: 'payload-changed' | 'message-id-rotated' };
|
|
45
|
-
|
|
46
|
-
export const CONFIRM_TOKEN_TTL_DEFAULT_SECONDS = 600;
|
|
47
|
-
|
|
48
|
-
const PREFIX = 'gct1';
|
|
49
|
-
|
|
50
|
-
type Claims = { t: string; a: string; g: string; r?: string; h: string; iat: number; exp: number; n: string };
|
|
51
|
-
|
|
52
|
-
let secret: Buffer | undefined;
|
|
53
|
-
// nonce → expiry (ms). Entries are dropped once they would have expired
|
|
54
|
-
// anyway, so the set is bounded by the tokens spent within one TTL. In memory
|
|
55
|
-
// only: see the header on GOG_CONFIRM_SECRET and restarts.
|
|
56
|
-
const spent = new Map<string, number>();
|
|
57
|
-
|
|
58
|
-
/** GOG_SEND_CONFIRM_FALLBACK=token turns the fallback on. Anything else is off. */
|
|
59
|
-
export function sendConfirmFallbackEnabled(): boolean {
|
|
60
|
-
return readEnvVar('GOG_SEND_CONFIRM_FALLBACK')?.trim().toLowerCase() === 'token';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function confirmTokenTtlSeconds(): number {
|
|
64
|
-
const raw = readEnvVar('GOG_CONFIRM_TTL_SECONDS')?.trim();
|
|
65
|
-
if (raw && /^\d+$/.test(raw)) {
|
|
66
|
-
const n = Number(raw);
|
|
67
|
-
if (n > 0) return n;
|
|
68
|
-
}
|
|
69
|
-
return CONFIRM_TOKEN_TTL_DEFAULT_SECONDS;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// GOG_CONFIRM_SECRET ends in _SECRET, so runner.ts strips it from every gog
|
|
73
|
-
// child — it never leaves this process.
|
|
74
|
-
function hmacKey(): Buffer {
|
|
75
|
-
if (!secret) {
|
|
76
|
-
const configured = readEnvVar('GOG_CONFIRM_SECRET');
|
|
77
|
-
secret = configured ? Buffer.from(configured, 'utf8') : randomBytes(32);
|
|
78
|
-
}
|
|
79
|
-
return secret;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Test seam: forget the secret (a "restart") and every spent token. */
|
|
26
|
+
/** Test seam: forget every spent token. */
|
|
83
27
|
export function resetConfirmTokenState(): void {
|
|
84
|
-
secret = undefined;
|
|
85
28
|
spent.clear();
|
|
86
29
|
}
|
|
87
|
-
|
|
88
|
-
// Sorted keys, undefined dropped: the same payload re-read on phase 2 must
|
|
89
|
-
// hash identically however its object was assembled.
|
|
90
|
-
function canonicalize(value: unknown): unknown {
|
|
91
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
92
|
-
if (value && typeof value === 'object') {
|
|
93
|
-
const out: Record<string, unknown> = {};
|
|
94
|
-
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
95
|
-
const v = (value as Record<string, unknown>)[key];
|
|
96
|
-
if (v !== undefined) out[key] = canonicalize(v);
|
|
97
|
-
}
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** SHA-256 (hex) of the canonical JSON of a send payload. */
|
|
104
|
-
export function hashSendPayload(payload: unknown): string {
|
|
105
|
-
return createHash('sha256').update(JSON.stringify(canonicalize(payload))).digest('hex');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sign(body: string): string {
|
|
109
|
-
return createHmac('sha256', hmacKey()).update(`${PREFIX}.${body}`).digest('base64url');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function issueConfirmToken(binding: ConfirmBinding, now = Date.now()): { token: string; expiresAt: string } {
|
|
113
|
-
const exp = now + confirmTokenTtlSeconds() * 1000;
|
|
114
|
-
const claims: Claims = {
|
|
115
|
-
t: binding.tool,
|
|
116
|
-
a: binding.account,
|
|
117
|
-
g: binding.target,
|
|
118
|
-
...(binding.revision === undefined ? {} : { r: binding.revision }),
|
|
119
|
-
h: binding.payloadHash,
|
|
120
|
-
iat: now,
|
|
121
|
-
exp,
|
|
122
|
-
n: randomBytes(16).toString('base64url'),
|
|
123
|
-
};
|
|
124
|
-
const body = Buffer.from(JSON.stringify(claims), 'utf8').toString('base64url');
|
|
125
|
-
return { token: `${PREFIX}.${body}.${sign(body)}`, expiresAt: new Date(exp).toISOString() };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function signatureMatches(body: string, sig: string): boolean {
|
|
129
|
-
const expected = Buffer.from(sign(body), 'utf8');
|
|
130
|
-
const actual = Buffer.from(sig, 'utf8');
|
|
131
|
-
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseClaims(body: string): Claims | undefined {
|
|
135
|
-
try {
|
|
136
|
-
return JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as Claims;
|
|
137
|
-
} catch {
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Check a phase-2 token against the binding recomputed from a fresh re-read.
|
|
144
|
-
* Only an `ok` verdict consumes the token; a DRAFT_CHANGED leaves it unspent,
|
|
145
|
-
* because the approval it carries is still true of the content it names.
|
|
146
|
-
*/
|
|
147
|
-
export function verifyConfirmToken(token: string, binding: ConfirmBinding, now = Date.now()): ConfirmTokenVerdict {
|
|
148
|
-
for (const [nonce, exp] of spent) if (exp < now) spent.delete(nonce);
|
|
149
|
-
|
|
150
|
-
const parts = token.split('.');
|
|
151
|
-
if (parts.length !== 3 || parts[0] !== PREFIX || !parts[1] || !parts[2]) return { ok: false, error: 'TOKEN_INVALID' };
|
|
152
|
-
const [, body, sig] = parts as [string, string, string];
|
|
153
|
-
if (!signatureMatches(body, sig)) return { ok: false, error: 'TOKEN_INVALID' };
|
|
154
|
-
const claims = parseClaims(body);
|
|
155
|
-
if (!claims) return { ok: false, error: 'TOKEN_INVALID' };
|
|
156
|
-
|
|
157
|
-
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
158
|
-
return { ok: false, error: 'TOKEN_INVALID' };
|
|
159
|
-
}
|
|
160
|
-
if (spent.has(claims.n)) return { ok: false, error: 'TOKEN_REUSED' };
|
|
161
|
-
if (now > claims.exp) return { ok: false, error: 'TOKEN_EXPIRED' };
|
|
162
|
-
if (claims.r !== binding.revision) return { ok: false, error: 'DRAFT_CHANGED', reason: 'message-id-rotated' };
|
|
163
|
-
if (claims.h !== binding.payloadHash) return { ok: false, error: 'DRAFT_CHANGED', reason: 'payload-changed' };
|
|
164
|
-
|
|
165
|
-
spent.set(claims.n, claims.exp);
|
|
166
|
-
return { ok: true };
|
|
167
|
-
}
|
package/src/tools/api.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { assertSafeForwardedArgs } from '../arg-guard.js';
|
|
|
6
6
|
import { confineAtFile } from '../file-roots.js';
|
|
7
7
|
import { pos } from '../argv.js';
|
|
8
8
|
import type { GogArg } from '../runner.js';
|
|
9
|
-
import { gatedElsewhere } from '../dispatch-confirmation.js';
|
|
9
|
+
import { BODY_PREVIEW_MAX, bodyPreview, CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, gatedElsewhere, requireDispatchConfirmation } from '../dispatch-confirmation.js';
|
|
10
10
|
|
|
11
11
|
// Gmail methods gog_api_call refuses outright (audit SEC-2). `allowWrite` is a
|
|
12
12
|
// boolean the MODEL sets, so it cannot stand in for the user's confirmation of
|
|
@@ -20,17 +20,38 @@ const GMAIL_API_BLOCKED = /(?:\.send$|forwarding|filters\.create|filters\.update
|
|
|
20
20
|
// the dedicated tool asks the user, so the raw API method must not be a way
|
|
21
21
|
// around it. Anchored on a `.` or the start so a Discovery id with the API
|
|
22
22
|
// prefix (`chat.spaces.messages.create`) matches too.
|
|
23
|
+
// Fleet audit 2026-09-24 SEC-6 added the methods that notify someone, grant
|
|
24
|
+
// access or run code under the account's authority (moving an event with
|
|
25
|
+
// sendUpdates, roster adds, returning work, posting coursework, comments,
|
|
26
|
+
// send-as aliases, the vacation responder, member-seeded spaces, script runs).
|
|
23
27
|
const DISPATCH_API_BLOCKED: Record<string, Array<{ method: RegExp; does: string; tool: string }>> = {
|
|
24
|
-
chat: [
|
|
25
|
-
|
|
28
|
+
chat: [
|
|
29
|
+
{ method: /(?:^|\.)spaces\.messages\.create$/i, does: 'posts a Chat message', tool: 'gog_chat_messages_send / gog_chat_dm_send' },
|
|
30
|
+
{ method: /(?:^|\.)spaces\.(?:setup|members\.create)$/i, does: 'adds people to a space', tool: 'gog_chat_spaces_create' },
|
|
31
|
+
],
|
|
32
|
+
drive: [
|
|
33
|
+
{ method: /(?:^|\.)permissions\.(?:create|update)$/i, does: 'grants access to a file', tool: 'gog_drive_share' },
|
|
34
|
+
{ method: /(?:^|\.)comments\.create$/i, does: 'notifies the file\'s owner and anyone it mentions', tool: 'gog_drive_comments_add / gog_docs_comments_add' },
|
|
35
|
+
{ method: /(?:^|\.)replies\.create$/i, does: 'notifies the comment thread', tool: 'gog_drive_comments_reply / gog_docs_comments_reply' },
|
|
36
|
+
],
|
|
26
37
|
classroom: [
|
|
27
38
|
{ method: /(?:^|\.)courses\.announcements\.create$/i, does: 'posts to a class', tool: 'gog_classroom_announcements_create' },
|
|
28
39
|
{ method: /(?:^|\.)invitations\.create$/i, does: 'invites someone to a class', tool: 'gog_classroom_invitations_create' },
|
|
40
|
+
{ method: /(?:^|\.)courses\.students\.create$/i, does: 'adds someone to a class', tool: 'gog_classroom_students_add' },
|
|
41
|
+
{ method: /(?:^|\.)courses\.teachers\.create$/i, does: 'gives someone teacher access to a class', tool: 'gog_classroom_teachers_add' },
|
|
42
|
+
{ method: /(?:^|\.)courses\.coursework\.create$/i, does: 'posts coursework to a class', tool: 'gog_classroom_coursework_create' },
|
|
43
|
+
{ method: /(?:^|\.)studentsubmissions\.return$/i, does: 'returns work to a student', tool: 'gog_classroom_submissions_return' },
|
|
29
44
|
],
|
|
30
45
|
calendar: [
|
|
31
46
|
{ method: /(?:^|\.)events\.(?:insert|import|quickadd)$/i, does: 'can put an event on guests\' calendars', tool: 'gog_calendar_create' },
|
|
32
47
|
{ method: /(?:^|\.)events\.(?:update|patch)$/i, does: 'can change what guests see', tool: 'gog_calendar_update / gog_calendar_respond' },
|
|
48
|
+
{ method: /(?:^|\.)events\.move$/i, does: 'can email every guest', tool: 'gog_calendar_move' },
|
|
33
49
|
],
|
|
50
|
+
gmail: [
|
|
51
|
+
{ method: /(?:^|\.)settings\.sendas\.create$/i, does: 'makes Google email the address and adds a sending identity', tool: 'gog_gmail_sendas_create' },
|
|
52
|
+
{ method: /(?:^|\.)settings\.updatevacation$/i, does: 'can turn on an auto-reply to every sender', tool: 'gog_gmail_vacation_update' },
|
|
53
|
+
],
|
|
54
|
+
script: [{ method: /(?:^|\.)scripts\.run$/i, does: 'executes code with this account\'s authority', tool: 'gog_appscript_run_function' }],
|
|
34
55
|
};
|
|
35
56
|
|
|
36
57
|
export function refusedApiCall(api: string, method: string): string | undefined {
|
|
@@ -44,10 +65,25 @@ export function refusedApiCall(api: string, method: string): string | undefined
|
|
|
44
65
|
return hit ? gatedElsewhere(`${name} ${m}`, 'gog_api_call', hit.does, hit.tool) : undefined;
|
|
45
66
|
}
|
|
46
67
|
|
|
68
|
+
/**
|
|
69
|
+
* A params/body string as the confirmation prompt shows it: parsed, when it is
|
|
70
|
+
* JSON, so the user reads a structure rather than an escaped string; otherwise
|
|
71
|
+
* (an `@file` reference, a typo) the string itself. Bounded like every other
|
|
72
|
+
* body on the rail — the token binds the FULL string regardless.
|
|
73
|
+
*/
|
|
74
|
+
export function previewJson(text: string): unknown {
|
|
75
|
+
if (text.length > BODY_PREVIEW_MAX) return bodyPreview(text);
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
} catch {
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
// Generic Google Discovery API access (gog 0.31). gog_api_list / gog_api_describe
|
|
48
84
|
// are read-only Discovery lookups; gog_api_call is a Discovery-backed escape
|
|
49
85
|
// hatch for any method gog has no dedicated subcommand for — guarded by an
|
|
50
|
-
// explicit write opt-in
|
|
86
|
+
// explicit write opt-in, a dry-run preview, and the dispatch rail.
|
|
51
87
|
export function registerApiTools(server: McpServer): void {
|
|
52
88
|
server.registerTool('gog_api_list', {
|
|
53
89
|
description: 'List the Google Discovery APIs available for gog_api_call / gog_api_describe (name + version + title).',
|
|
@@ -78,7 +114,7 @@ export function registerApiTools(server: McpServer): void {
|
|
|
78
114
|
});
|
|
79
115
|
|
|
80
116
|
server.registerTool('gog_api_call', {
|
|
81
|
-
description: 'Call any Discovery-described Google API method directly — an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true
|
|
117
|
+
description: 'Call any Discovery-described Google API method directly — an escape hatch for endpoints gog has no dedicated tool for. Find the exact api/version/method/params with gog_api_describe first. Read methods (GET/LIST) run as-is. Mutating methods (POST/PUT/PATCH/DELETE) are refused unless you set allowWrite=true, and every write then asks the MCP host to show the user a confirmation prompt with the exact api/version/method/params/body before anything is sent; set dryRun=true to print the intended request without sending it (no prompt, no changes). Gmail send and forwarding methods (users.messages.send, users.drafts.send, forwarding/auto-forwarding, filters, delegates) are refused outright — use the dedicated gog_gmail_* tools, which ask the user to confirm. So are the other methods a dedicated tool asks about with a better preview: Chat spaces.messages.create, Drive permissions.create/update, Classroom courses.announcements.create and invitations.create, and Calendar events.insert/import/quickAdd/update/patch.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
82
118
|
annotations: { destructiveHint: true },
|
|
83
119
|
inputSchema: z.object({
|
|
84
120
|
api: z.string().describe('Discovery API name (e.g. drive, gmail, calendar)'),
|
|
@@ -87,11 +123,12 @@ export function registerApiTools(server: McpServer): void {
|
|
|
87
123
|
params: z.string().optional().describe('Query/path parameters as a JSON object string (e.g. {"fileId":"abc","fields":"name"})'),
|
|
88
124
|
body: z.string().optional().describe('Request body as a JSON string (for write methods)'),
|
|
89
125
|
scope: z.string().optional().describe('Override the OAuth scope used for the call'),
|
|
90
|
-
allowWrite: z.boolean().optional().describe('Required to invoke a mutating method (POST/PUT/PATCH/DELETE). Without it, gog refuses write methods. Leave unset for read-only calls.'),
|
|
91
|
-
dryRun: z.boolean().optional().describe('Print the intended request and exit without sending it (no changes made)'),
|
|
126
|
+
allowWrite: z.boolean().optional().describe('Required to invoke a mutating method (POST/PUT/PATCH/DELETE); the user is asked to confirm the exact request first. Without it, gog refuses write methods. Leave unset for read-only calls.'),
|
|
127
|
+
dryRun: z.boolean().optional().describe('Print the intended request and exit without sending it (no changes made, no confirmation prompt)'),
|
|
92
128
|
account: accountParam,
|
|
129
|
+
confirmToken: confirmTokenParam,
|
|
93
130
|
}),
|
|
94
|
-
}, async ({ api, version, method, params, body, scope, allowWrite, dryRun, account }) => {
|
|
131
|
+
}, async ({ api, version, method, params, body, scope, allowWrite, dryRun, account, confirmToken }, ctx) => {
|
|
95
132
|
try {
|
|
96
133
|
assertSafeForwardedArgs([api, version, method]);
|
|
97
134
|
const refusal = refusedApiCall(api, method);
|
|
@@ -103,6 +140,35 @@ export function registerApiTools(server: McpServer): void {
|
|
|
103
140
|
} catch (err) {
|
|
104
141
|
return errorResult(errorText(err));
|
|
105
142
|
}
|
|
143
|
+
// SEC-2 (fleet-audit #931): allowWrite is a boolean the MODEL sets, and
|
|
144
|
+
// the --force below skips gog's own confirmation, so a method blocklist
|
|
145
|
+
// could never be complete (events.move/delete with sendUpdates=all,
|
|
146
|
+
// acl.insert, settings.sendAs.create, updateVacation, batchDelete, ...).
|
|
147
|
+
// Every write that will actually be sent asks the user first, about the
|
|
148
|
+
// exact api/method/params/body, through the same rail as the dedicated
|
|
149
|
+
// tools. A dry run sends nothing and is not gated; a write without
|
|
150
|
+
// allowWrite is refused by gog itself.
|
|
151
|
+
if (allowWrite && !dryRun) {
|
|
152
|
+
const request = { api, version, method, params, body, scope };
|
|
153
|
+
const view: Record<string, unknown> = { api, version, method };
|
|
154
|
+
if (params !== undefined) view.params = previewJson(params);
|
|
155
|
+
if (body !== undefined) view.body = previewJson(body);
|
|
156
|
+
if (scope !== undefined) view.scope = scope;
|
|
157
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
158
|
+
action: 'api.call',
|
|
159
|
+
message: `Review and confirm this raw Google API write (${api} ${version} ${method}):`,
|
|
160
|
+
confirmationLabel: 'Confirm that this API method should be invoked with exactly these parameters.',
|
|
161
|
+
details: view,
|
|
162
|
+
unsupportedNote: 'Set dryRun=true to see the request gog would send without sending it, or use the dedicated gog_* tool for this operation.',
|
|
163
|
+
fallback: {
|
|
164
|
+
tool: 'gog_api_call',
|
|
165
|
+
account,
|
|
166
|
+
confirmToken,
|
|
167
|
+
subject: () => ({ target: `${api}/${version}/${method}`, payload: request, preview: view }),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
if (confirmation) return confirmation;
|
|
171
|
+
}
|
|
106
172
|
const args: GogArg[] = ['api', 'call', pos(api), pos(version), pos(method)];
|
|
107
173
|
if (params) args.push(`--params=${params}`);
|
|
108
174
|
if (body) args.push(`--body=${body}`);
|
package/src/tools/appscript.ts
CHANGED
|
@@ -10,6 +10,41 @@ import {
|
|
|
10
10
|
import { pos } from '../argv.js';
|
|
11
11
|
import { confinePath } from '../file-roots.js';
|
|
12
12
|
import type { GogArg } from '../runner.js';
|
|
13
|
+
import {
|
|
14
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
15
|
+
confirmTokenParam,
|
|
16
|
+
gatedElsewhere,
|
|
17
|
+
requireDispatchConfirmation,
|
|
18
|
+
resultText,
|
|
19
|
+
senderPreview,
|
|
20
|
+
} from '../dispatch-confirmation.js';
|
|
21
|
+
|
|
22
|
+
/** gog_appscript_run must not execute code that gog_appscript_run_function would ask about. */
|
|
23
|
+
export function vetAppScriptRun(subcommand: string, _args: readonly string[]): string | undefined {
|
|
24
|
+
return subcommand.toLowerCase() === 'run'
|
|
25
|
+
? gatedElsewhere('gog appscript run', 'gog_appscript_run', 'executes code with this account\'s authority', 'gog_appscript_run_function')
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The project a run executes, for its confirmation prompt: a title the user
|
|
31
|
+
* recognises beside the opaque id. `updateTime` is kept apart as the token's
|
|
32
|
+
* revision, so code saved between the two phases is DRAFT_CHANGED. Unreadable
|
|
33
|
+
* output names nothing rather than throwing.
|
|
34
|
+
*/
|
|
35
|
+
export function projectSnapshot(raw: string, scriptId: string): { scriptId: string; title?: string; updateTime?: string } {
|
|
36
|
+
let project: { title?: unknown; updateTime?: unknown } | undefined;
|
|
37
|
+
try {
|
|
38
|
+
project = (JSON.parse(raw) as { project?: typeof project } | null)?.project;
|
|
39
|
+
} catch {
|
|
40
|
+
project = undefined;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
scriptId,
|
|
44
|
+
...(typeof project?.title === 'string' ? { title: project.title } : {}),
|
|
45
|
+
...(typeof project?.updateTime === 'string' ? { updateTime: project.updateTime } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
13
48
|
|
|
14
49
|
// Google Apps Script (gog >= 0.38.0 for pull/deployments/versions).
|
|
15
50
|
//
|
|
@@ -140,7 +175,9 @@ export function registerAppScriptTools(server: McpServer): void {
|
|
|
140
175
|
+ 'Requires the project to be deployed as an API executable and to share the OAuth client with the calling '
|
|
141
176
|
+ 'credentials, otherwise Google refuses regardless of scopes. devMode runs the latest saved code instead of the '
|
|
142
177
|
+ 'deployed version, and only works if the account owns the script. '
|
|
143
|
-
+ 'This is NOT the escape hatch — gog_appscript_run is that.
|
|
178
|
+
+ 'This is NOT the escape hatch — gog_appscript_run is that. Because the wrapper cannot tell what the code will '
|
|
179
|
+
+ 'do, it reads the project and asks the MCP host to show the user a confirmation prompt with the project, the '
|
|
180
|
+
+ 'function and its arguments first; nothing runs unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION + apiEnableNote,
|
|
144
181
|
annotations: { destructiveHint: true },
|
|
145
182
|
inputSchema: z.object({
|
|
146
183
|
scriptId: scriptIdParam,
|
|
@@ -148,12 +185,14 @@ export function registerAppScriptTools(server: McpServer): void {
|
|
|
148
185
|
params: z.string().optional().describe('Function parameters as a JSON ARRAY of positional arguments, e.g. \'["a", 1]\' — not an object'),
|
|
149
186
|
devMode: z.boolean().optional().describe('Run the latest saved code rather than the deployed version (owner only)'),
|
|
150
187
|
account: accountParam,
|
|
188
|
+
confirmToken: confirmTokenParam,
|
|
151
189
|
}),
|
|
152
|
-
}, async ({ scriptId, functionName, params, devMode, account }) => {
|
|
190
|
+
}, async ({ scriptId, functionName, params, devMode, account, confirmToken }, ctx) => {
|
|
153
191
|
// gog passes --params through to the API as-is, so a malformed value comes
|
|
154
192
|
// back as a Google error about the request body rather than about the
|
|
155
193
|
// argument the caller actually got wrong. Checking the shape here is what
|
|
156
194
|
// turns "invalid argument" into "params must be a JSON array".
|
|
195
|
+
let parsedParams: unknown[] | undefined;
|
|
157
196
|
if (params !== undefined) {
|
|
158
197
|
let parsed: unknown;
|
|
159
198
|
try {
|
|
@@ -164,7 +203,28 @@ export function registerAppScriptTools(server: McpServer): void {
|
|
|
164
203
|
if (!Array.isArray(parsed)) {
|
|
165
204
|
throw new Error(`params must be a JSON ARRAY of positional arguments, e.g. '["a", 1]' — Apps Script takes positional arguments, not named ones. Received: ${params}`);
|
|
166
205
|
}
|
|
206
|
+
parsedParams = parsed;
|
|
167
207
|
}
|
|
208
|
+
// Read on every call: the prompt names the project, and on the token
|
|
209
|
+
// fallback's phase 2 this is the re-read the token is checked against.
|
|
210
|
+
const got = await runOrDiagnose(['appscript', 'get', pos(scriptId)], { account });
|
|
211
|
+
if (got.isError) return got;
|
|
212
|
+
const { updateTime, ...project } = projectSnapshot(resultText(got), scriptId);
|
|
213
|
+
const execution = { project, functionName, params: parsedParams, devMode: Boolean(devMode), runsAs: senderPreview(account) };
|
|
214
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
215
|
+
action: 'appscript.run-function',
|
|
216
|
+
message: 'Review and confirm running this Apps Script function with the account\'s full authority:',
|
|
217
|
+
confirmationLabel: 'Confirm that this code should run now.',
|
|
218
|
+
details: execution,
|
|
219
|
+
unsupportedNote: 'Ask the user to run it themselves from the Apps Script editor.',
|
|
220
|
+
fallback: {
|
|
221
|
+
tool: 'gog_appscript_run_function',
|
|
222
|
+
account,
|
|
223
|
+
confirmToken,
|
|
224
|
+
subject: () => ({ target: `${scriptId}/${functionName}`, revision: updateTime, payload: execution, preview: execution }),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
if (confirmation) return confirmation;
|
|
168
228
|
const args: GogArg[] = ['appscript', 'run', pos(scriptId), pos(functionName)];
|
|
169
229
|
if (params !== undefined) args.push(`--params=${params}`);
|
|
170
230
|
if (devMode) args.push('--dev-mode');
|
|
@@ -174,6 +234,7 @@ export function registerAppScriptTools(server: McpServer): void {
|
|
|
174
234
|
registerRunTool(server, {
|
|
175
235
|
service: 'appscript',
|
|
176
236
|
examples: '"get", "content", "deployments"',
|
|
237
|
+
vet: vetAppScriptRun,
|
|
177
238
|
note: 'To execute a function, use gog_appscript_run_function — this tool is the generic escape hatch.',
|
|
178
239
|
});
|
|
179
240
|
}
|
package/src/tools/calendar.ts
CHANGED
|
@@ -5,22 +5,60 @@ import { accountParam, runOrDiagnose, registerRunTool, pageTokenParam, pageAlias
|
|
|
5
5
|
import { annotateTruncatedList } from '../pagination.js';
|
|
6
6
|
import { pos } from '../argv.js';
|
|
7
7
|
import type { GogArg } from '../runner.js';
|
|
8
|
-
import { CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, gatedElsewhere, requireDispatchConfirmation, resultText } from '../dispatch-confirmation.js';
|
|
8
|
+
import { CONFIRM_FALLBACK_DESCRIPTION, confirmTokenParam, flagValue, gatedElsewhere, refusedInRun, requireDispatchConfirmation, resultText } from '../dispatch-confirmation.js';
|
|
9
9
|
|
|
10
|
-
// gog's spellings (internal/cmd/calendar.go
|
|
11
|
-
// an event has guests, so it refuses these
|
|
12
|
-
// only when someone else would see the
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
// gog's spellings (internal/cmd/calendar.go; aliases per `gog schema` 0.41.0).
|
|
11
|
+
// The run tool cannot tell whether an event has guests, so it refuses these
|
|
12
|
+
// outright; the dedicated tools ask only when someone else would see the
|
|
13
|
+
// change. Move, delete, delete-calendar and out-of-office joined in the fleet
|
|
14
|
+
// audit of 2026-09-24 (SEC-4 #933, SEC-6): --send-updates on a move emails
|
|
15
|
+
// every guest, out-of-office auto-declines conflicting invitations by default,
|
|
16
|
+
// and delete's --scope defaults to the whole recurring series.
|
|
17
|
+
const ASKS = (tool: string) => `Use ${tool}, which asks the user to confirm.`;
|
|
18
|
+
const CALENDAR_REFUSED: Record<string, { does: string; instead: string }> = {
|
|
19
|
+
create: { does: 'can change what guests see', instead: ASKS('gog_calendar_create') },
|
|
20
|
+
add: { does: 'can change what guests see', instead: ASKS('gog_calendar_create') },
|
|
21
|
+
new: { does: 'can change what guests see', instead: ASKS('gog_calendar_create') },
|
|
22
|
+
update: { does: 'can change what guests see', instead: ASKS('gog_calendar_update') },
|
|
23
|
+
edit: { does: 'can change what guests see', instead: ASKS('gog_calendar_update') },
|
|
24
|
+
set: { does: 'can change what guests see', instead: ASKS('gog_calendar_update') },
|
|
25
|
+
respond: { does: 'can change what guests see', instead: ASKS('gog_calendar_respond') },
|
|
26
|
+
rsvp: { does: 'can change what guests see', instead: ASKS('gog_calendar_respond') },
|
|
27
|
+
reply: { does: 'can change what guests see', instead: ASKS('gog_calendar_respond') },
|
|
28
|
+
move: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS('gog_calendar_move') },
|
|
29
|
+
transfer: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS('gog_calendar_move') },
|
|
30
|
+
delete: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS('gog_calendar_delete') },
|
|
31
|
+
del: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS('gog_calendar_delete') },
|
|
32
|
+
rm: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS('gog_calendar_delete') },
|
|
33
|
+
remove: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS('gog_calendar_delete') },
|
|
34
|
+
'delete-calendar': { does: 'deletes a calendar and every event on it', instead: ASKS('gog_calendar_delete_calendar') },
|
|
35
|
+
'out-of-office': { does: 'auto-declines invitations and notifies their organizers', instead: ASKS('gog_calendar_out_of_office') },
|
|
36
|
+
ooo: { does: 'auto-declines invitations and notifies their organizers', instead: ASKS('gog_calendar_out_of_office') },
|
|
17
37
|
};
|
|
18
38
|
|
|
19
39
|
/** gog_calendar_run must not make the changes gog_calendar_create/update/respond would ask about. */
|
|
20
|
-
export function vetCalendarRun(subcommand: string,
|
|
40
|
+
export function vetCalendarRun(subcommand: string, args: readonly string[]): string | undefined {
|
|
21
41
|
const sub = subcommand.toLowerCase();
|
|
22
|
-
const
|
|
23
|
-
|
|
42
|
+
const what = `gog calendar ${sub}`;
|
|
43
|
+
const via = 'gog_calendar_run';
|
|
44
|
+
if (Object.hasOwn(CALENDAR_REFUSED, sub)) {
|
|
45
|
+
const { does, instead } = CALENDAR_REFUSED[sub]!;
|
|
46
|
+
return refusedInRun(what, via, does, instead);
|
|
47
|
+
}
|
|
48
|
+
// `propose-time` alone generates a URL. With --decline (or --comment, which
|
|
49
|
+
// implies it) it declines the event and notifies the organizer — exactly
|
|
50
|
+
// what gog_calendar_respond asks about (SEC-4, fleet-audit #933).
|
|
51
|
+
if (sub === 'propose-time' && (flagValue(args, 'decline') !== undefined || flagValue(args, 'comment') !== undefined)) {
|
|
52
|
+
return gatedElsewhere(`${what} --decline`, via, 'declines the event and notifies the organizer', 'gog_calendar_respond');
|
|
53
|
+
}
|
|
54
|
+
// A Focus Time block auto-declines every conflicting invitation by default
|
|
55
|
+
// (gog's --auto-decline defaults to all) and there is no dedicated tool for
|
|
56
|
+
// it; one that declines nobody is a private block and passes.
|
|
57
|
+
if ((sub === 'focus-time' || sub === 'focus') && flagValue(args, 'auto-decline')?.toLowerCase() !== 'none') {
|
|
58
|
+
return refusedInRun(what, via, 'auto-declines invitations and notifies their organizers (gog defaults --auto-decline to all)',
|
|
59
|
+
'Pass --auto-decline=none, or ask the user to create it from Google Calendar.');
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
24
62
|
}
|
|
25
63
|
|
|
26
64
|
// Reminder params, shared by create and update (gog >= 0.38.0 for
|
|
@@ -70,11 +108,13 @@ export function eventSnapshot(raw: string): {
|
|
|
70
108
|
end?: string;
|
|
71
109
|
organizer?: string;
|
|
72
110
|
guests: string[];
|
|
111
|
+
recurring?: true;
|
|
73
112
|
etag?: string;
|
|
74
113
|
} {
|
|
75
114
|
let event: {
|
|
76
115
|
summary?: unknown; start?: EventTime; end?: EventTime; etag?: unknown;
|
|
77
116
|
organizer?: { email?: unknown }; attendees?: EventAttendee[];
|
|
117
|
+
recurrence?: unknown; recurringEventId?: unknown;
|
|
78
118
|
} | undefined;
|
|
79
119
|
try {
|
|
80
120
|
event = (JSON.parse(raw) as { event?: typeof event } | null)?.event;
|
|
@@ -91,6 +131,10 @@ export function eventSnapshot(raw: string): {
|
|
|
91
131
|
guests: (Array.isArray(event?.attendees) ? event.attendees : [])
|
|
92
132
|
.filter((a) => a.self !== true && a.resource !== true && typeof a.email === 'string')
|
|
93
133
|
.map((a) => a.email as string),
|
|
134
|
+
// A series, or one instance of one: gog's delete --scope defaults to all.
|
|
135
|
+
...((Array.isArray(event?.recurrence) && event.recurrence.length > 0) || typeof event?.recurringEventId === 'string'
|
|
136
|
+
? { recurring: true as const }
|
|
137
|
+
: {}),
|
|
94
138
|
etag: str(event?.etag),
|
|
95
139
|
};
|
|
96
140
|
}
|
|
@@ -357,14 +401,40 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
357
401
|
});
|
|
358
402
|
|
|
359
403
|
server.registerTool('gog_calendar_delete', {
|
|
360
|
-
description: 'Delete a calendar event.',
|
|
404
|
+
description: 'Delete a calendar event. It disappears from every guest\'s calendar too, and for a recurring event gog '
|
|
405
|
+
+ 'deletes the WHOLE series, so when the event has guests or recurs this reads it and asks the MCP host to show '
|
|
406
|
+
+ 'the user a confirmation prompt with the event as it stands; a guest-free, one-off event is deleted without asking.'
|
|
407
|
+
+ CONFIRM_FALLBACK_DESCRIPTION,
|
|
361
408
|
annotations: { destructiveHint: true },
|
|
362
409
|
inputSchema: z.object({
|
|
363
410
|
calendarId: z.string().describe('Calendar ID'),
|
|
364
411
|
eventId: z.string().describe('Event ID'),
|
|
365
412
|
account: accountParam,
|
|
413
|
+
confirmToken: confirmTokenParam,
|
|
366
414
|
}),
|
|
367
|
-
}, async ({ calendarId, eventId, account }) => {
|
|
415
|
+
}, async ({ calendarId, eventId, account, confirmToken }, ctx) => {
|
|
416
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
417
|
+
if (read.error) return read.error;
|
|
418
|
+
const { etag, ...event } = read.event;
|
|
419
|
+
if (event.guests.length > 0 || event.recurring) {
|
|
420
|
+
const view = { calendarId, eventId, event, scope: event.recurring ? 'the whole recurring series' : 'this event' };
|
|
421
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
422
|
+
action: 'calendar.delete',
|
|
423
|
+
message: event.recurring
|
|
424
|
+
? 'Review and confirm deleting this ENTIRE recurring series:'
|
|
425
|
+
: 'Review and confirm deleting this event from every guest\'s calendar:',
|
|
426
|
+
confirmationLabel: 'Confirm that this event should be deleted now.',
|
|
427
|
+
details: view,
|
|
428
|
+
unsupportedNote: 'Ask the user to delete it from Google Calendar.',
|
|
429
|
+
fallback: {
|
|
430
|
+
tool: 'gog_calendar_delete',
|
|
431
|
+
account,
|
|
432
|
+
confirmToken,
|
|
433
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view }),
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
if (confirmation) return confirmation;
|
|
437
|
+
}
|
|
368
438
|
// gog gates this delete behind a confirmation; the runner injects
|
|
369
439
|
// --no-input, so without --force it refuses at runtime.
|
|
370
440
|
return runOrDiagnose(['calendar', 'delete', pos(calendarId), pos(eventId), '--force'], { account });
|
package/src/tools/chat.ts
CHANGED
|
@@ -27,9 +27,20 @@ import {
|
|
|
27
27
|
// gog's spellings of `send` under `messages` and `dm` (internal/cmd/chat_messages.go, chat_dm.go).
|
|
28
28
|
const CHAT_SEND_WORDS = new Set(['send', 'create', 'post']);
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// gog's spellings of `spaces create` (`gog schema` 0.41.0).
|
|
31
|
+
const CHAT_SPACE_CREATE_WORDS = new Set(['create', 'add', 'new']);
|
|
32
|
+
|
|
33
|
+
/** gog_chat_run must not post, or add people to a space, where the dedicated tools would ask. */
|
|
31
34
|
export function vetChatRun(subcommand: string, args: readonly string[]): string | undefined {
|
|
32
35
|
const sub = subcommand.toLowerCase();
|
|
36
|
+
if (sub === 'spaces') {
|
|
37
|
+
// A member-less space reaches nobody; one seeded with --member adds and notifies them.
|
|
38
|
+
const word = hasCommandWord(args, CHAT_SPACE_CREATE_WORDS);
|
|
39
|
+
const withMembers = args.some((a) => /^--members?(?:=|$)/i.test(a));
|
|
40
|
+
return word && withMembers
|
|
41
|
+
? gatedElsewhere(`gog chat spaces ${word.toLowerCase()} --member`, 'gog_chat_run', 'adds people to a space', 'gog_chat_spaces_create')
|
|
42
|
+
: undefined;
|
|
43
|
+
}
|
|
33
44
|
if (sub !== 'messages' && sub !== 'dm') return undefined;
|
|
34
45
|
const word = hasCommandWord(args, CHAT_SEND_WORDS);
|
|
35
46
|
if (!word) return undefined;
|
|
@@ -114,17 +125,37 @@ export function registerChatTools(server: McpServer): void {
|
|
|
114
125
|
|
|
115
126
|
server.registerTool('gog_chat_spaces_create', {
|
|
116
127
|
description:
|
|
117
|
-
'Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified
|
|
118
|
-
+ '
|
|
128
|
+
'Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified, so '
|
|
129
|
+
+ 'with members this asks the MCP host to show the user a confirmation prompt with the space name and every member '
|
|
130
|
+
+ 'first; nothing is created unless they accept. A space with no members reaches nobody and is created without '
|
|
131
|
+
+ 'asking.' + CONFIRM_FALLBACK_DESCRIPTION + workspaceOnlyNote,
|
|
119
132
|
annotations: { destructiveHint: true },
|
|
120
133
|
inputSchema: z.object({
|
|
121
134
|
displayName: z.string().describe('Display name for the new space'),
|
|
122
135
|
members: z.array(z.string()).optional().describe('Initial members, as email addresses or "users/..." resource names'),
|
|
123
136
|
account: accountParam,
|
|
137
|
+
confirmToken: confirmTokenParam,
|
|
124
138
|
}),
|
|
125
|
-
}, async ({ displayName, members, account }) => {
|
|
139
|
+
}, async ({ displayName, members, account, confirmToken }, ctx) => {
|
|
126
140
|
const args: GogArg[] = ['chat', 'spaces', 'create', pos(displayName)];
|
|
127
141
|
if (members) for (const member of members) args.push(`--member=${member}`);
|
|
142
|
+
if (members?.length) {
|
|
143
|
+
const space = { displayName, members };
|
|
144
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
145
|
+
action: 'chat.space-create',
|
|
146
|
+
message: 'Review and confirm this Chat space — every member is added and notified:',
|
|
147
|
+
confirmationLabel: 'Confirm that these people should be added to a new space now.',
|
|
148
|
+
details: space,
|
|
149
|
+
unsupportedNote: 'Create the space without members instead; the user can add people from Google Chat.',
|
|
150
|
+
fallback: {
|
|
151
|
+
tool: 'gog_chat_spaces_create',
|
|
152
|
+
account,
|
|
153
|
+
confirmToken,
|
|
154
|
+
subject: () => ({ target: displayName, payload: space, preview: space }),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (confirmation) return confirmation;
|
|
158
|
+
}
|
|
128
159
|
return runOrDiagnose(args, { account });
|
|
129
160
|
});
|
|
130
161
|
|