@littlebearapps/outlook-assistant 3.12.1 → 3.14.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.
- package/.env.example +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk-class map: the single source of truth for how risky each tool and
|
|
3
|
+
* action is (#270).
|
|
4
|
+
*
|
|
5
|
+
* Every tool, and every value of its `action` enum, is mapped to one class:
|
|
6
|
+
* - read no change anywhere
|
|
7
|
+
* - reversible changes only the user's own data, and can be undone
|
|
8
|
+
* - outward reaches other people (sends, invites, cancellations)
|
|
9
|
+
* - destructive deletes something that may not be recoverable
|
|
10
|
+
* - persistent keeps acting after the call (rules, forwarding, auto-replies)
|
|
11
|
+
*
|
|
12
|
+
* Tool annotations are derived from this map (see riskAnnotations), so the
|
|
13
|
+
* hints can't drift from the classes. The server instructions, read-only
|
|
14
|
+
* mode, the plugin hook and the skill's risk table build on it too. A test
|
|
15
|
+
* fails if any tool or action is unclassified, so new surfaces (OneDrive,
|
|
16
|
+
* To Do, Teams) must be classified on purpose.
|
|
17
|
+
*
|
|
18
|
+
* Per-tool flags:
|
|
19
|
+
* - untrustedContent results can carry content written by other people
|
|
20
|
+
* (email bodies, attachments, directory data), which
|
|
21
|
+
* makes the tool open-world (#92)
|
|
22
|
+
* - idempotent repeating a write with the same arguments has no
|
|
23
|
+
* further effect (reads are always idempotent)
|
|
24
|
+
* - defaultAction the action the handler runs when a call leaves
|
|
25
|
+
* `action` out (only where the schema makes it
|
|
26
|
+
* optional), so such calls classify correctly
|
|
27
|
+
* - requiresUserInteraction
|
|
28
|
+
* publish Claude's `anthropic/requiresUserInteraction`
|
|
29
|
+
* flag, so Claude clients always ask before running it.
|
|
30
|
+
* Only for single-purpose tools whose every call reaches
|
|
31
|
+
* other people; mixed read/write tools never get it (D1)
|
|
32
|
+
*
|
|
33
|
+
* Pure data with no requires, so any module can load it.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const RISK_CLASSES = [
|
|
37
|
+
'read',
|
|
38
|
+
'reversible',
|
|
39
|
+
'outward',
|
|
40
|
+
'destructive',
|
|
41
|
+
'persistent',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const TOOL_RISK = {
|
|
45
|
+
auth: {
|
|
46
|
+
// Sign-in only writes the local token file.
|
|
47
|
+
actions: {
|
|
48
|
+
status: 'read',
|
|
49
|
+
authenticate: 'reversible',
|
|
50
|
+
'device-code-complete': 'reversible',
|
|
51
|
+
about: 'read',
|
|
52
|
+
},
|
|
53
|
+
defaultAction: 'status',
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Calendar
|
|
57
|
+
// Event subjects and previews come from external organisers.
|
|
58
|
+
'list-events': { default: 'read', untrustedContent: true },
|
|
59
|
+
// Saving an event with attendees sends them invitations.
|
|
60
|
+
'create-event': { default: 'outward', requiresUserInteraction: true },
|
|
61
|
+
'manage-event': {
|
|
62
|
+
actions: {
|
|
63
|
+
// Organiser updates are sent to attendees.
|
|
64
|
+
update: 'outward',
|
|
65
|
+
decline: 'outward',
|
|
66
|
+
cancel: 'outward',
|
|
67
|
+
// Graph sends cancellations when an organiser deletes a meeting.
|
|
68
|
+
delete: 'outward',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Email
|
|
73
|
+
'search-emails': { default: 'read', untrustedContent: true },
|
|
74
|
+
'read-email': { default: 'read', untrustedContent: true },
|
|
75
|
+
'send-email': { default: 'outward', requiresUserInteraction: true },
|
|
76
|
+
draft: {
|
|
77
|
+
actions: {
|
|
78
|
+
create: 'reversible',
|
|
79
|
+
update: 'reversible',
|
|
80
|
+
send: 'outward',
|
|
81
|
+
delete: 'destructive',
|
|
82
|
+
// reply/reply-all/forward only create a draft; nothing is sent.
|
|
83
|
+
reply: 'reversible',
|
|
84
|
+
'reply-all': 'reversible',
|
|
85
|
+
forward: 'reversible',
|
|
86
|
+
},
|
|
87
|
+
// Reply and forward drafts quote the original message.
|
|
88
|
+
untrustedContent: true,
|
|
89
|
+
},
|
|
90
|
+
'update-email': {
|
|
91
|
+
actions: {
|
|
92
|
+
'mark-read': 'reversible',
|
|
93
|
+
'mark-unread': 'reversible',
|
|
94
|
+
flag: 'reversible',
|
|
95
|
+
unflag: 'reversible',
|
|
96
|
+
complete: 'reversible',
|
|
97
|
+
},
|
|
98
|
+
idempotent: true,
|
|
99
|
+
},
|
|
100
|
+
attachments: {
|
|
101
|
+
// download writes a new local file (never overwrites).
|
|
102
|
+
actions: { list: 'read', view: 'read', download: 'reversible' },
|
|
103
|
+
defaultAction: 'list',
|
|
104
|
+
untrustedContent: true,
|
|
105
|
+
},
|
|
106
|
+
// Writes local files, and with overwrite: true can replace an existing one.
|
|
107
|
+
export: { default: 'destructive', untrustedContent: true },
|
|
108
|
+
// Echoes recipients' out-of-office messages.
|
|
109
|
+
'get-mail-tips': { default: 'read', untrustedContent: true },
|
|
110
|
+
|
|
111
|
+
// Folders
|
|
112
|
+
folders: {
|
|
113
|
+
actions: {
|
|
114
|
+
list: 'read',
|
|
115
|
+
create: 'reversible',
|
|
116
|
+
// Moves emails between folders.
|
|
117
|
+
move: 'reversible',
|
|
118
|
+
stats: 'read',
|
|
119
|
+
delete: 'destructive',
|
|
120
|
+
},
|
|
121
|
+
defaultAction: 'list',
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Rules: create/update/reorder change what happens to future mail,
|
|
125
|
+
// including forwarding it to other people.
|
|
126
|
+
'manage-rules': {
|
|
127
|
+
actions: {
|
|
128
|
+
list: 'read',
|
|
129
|
+
create: 'persistent',
|
|
130
|
+
update: 'persistent',
|
|
131
|
+
reorder: 'persistent',
|
|
132
|
+
delete: 'destructive',
|
|
133
|
+
},
|
|
134
|
+
defaultAction: 'list',
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Contacts
|
|
138
|
+
'manage-contact': {
|
|
139
|
+
actions: {
|
|
140
|
+
list: 'read',
|
|
141
|
+
search: 'read',
|
|
142
|
+
get: 'read',
|
|
143
|
+
create: 'reversible',
|
|
144
|
+
update: 'reversible',
|
|
145
|
+
delete: 'destructive',
|
|
146
|
+
},
|
|
147
|
+
defaultAction: 'list',
|
|
148
|
+
},
|
|
149
|
+
'search-people': { default: 'read', untrustedContent: true },
|
|
150
|
+
|
|
151
|
+
// Categories
|
|
152
|
+
'manage-category': {
|
|
153
|
+
actions: {
|
|
154
|
+
list: 'read',
|
|
155
|
+
create: 'reversible',
|
|
156
|
+
update: 'reversible',
|
|
157
|
+
set: 'reversible',
|
|
158
|
+
delete: 'destructive',
|
|
159
|
+
},
|
|
160
|
+
defaultAction: 'list',
|
|
161
|
+
},
|
|
162
|
+
'apply-category': {
|
|
163
|
+
actions: { set: 'reversible', add: 'reversible', remove: 'reversible' },
|
|
164
|
+
defaultAction: 'set',
|
|
165
|
+
idempotent: true,
|
|
166
|
+
},
|
|
167
|
+
'manage-focused-inbox': {
|
|
168
|
+
actions: { list: 'read', set: 'reversible', delete: 'destructive' },
|
|
169
|
+
defaultAction: 'list',
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Settings: auto-replies answer external senders until switched off.
|
|
173
|
+
'mailbox-settings': {
|
|
174
|
+
actions: {
|
|
175
|
+
get: 'read',
|
|
176
|
+
'set-auto-replies': 'persistent',
|
|
177
|
+
'set-working-hours': 'reversible',
|
|
178
|
+
},
|
|
179
|
+
defaultAction: 'get',
|
|
180
|
+
idempotent: true,
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// Advanced
|
|
184
|
+
'access-shared-mailbox': { default: 'read', untrustedContent: true },
|
|
185
|
+
'find-meeting-rooms': { default: 'read' },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Which calls genuinely honour `dryRun: true` (#274): `true` for a
|
|
190
|
+
* single-purpose tool, or the list of actions that return a preview and
|
|
191
|
+
* change nothing. The dispatcher refuses `dryRun: true` on every other call
|
|
192
|
+
* (including read actions) before the handler runs, so a "preview" can never
|
|
193
|
+
* really send, delete or change anything, and the plugin hook lets only these
|
|
194
|
+
* previews run without asking. A test checks this map against
|
|
195
|
+
* every tool whose schema has a `dryRun` property.
|
|
196
|
+
*/
|
|
197
|
+
const DRY_RUN_ACTIONS = {
|
|
198
|
+
'create-event': true,
|
|
199
|
+
'manage-event': ['update', 'decline', 'cancel', 'delete'],
|
|
200
|
+
'send-email': true,
|
|
201
|
+
draft: ['create'],
|
|
202
|
+
folders: ['delete'],
|
|
203
|
+
'manage-rules': ['create', 'update'],
|
|
204
|
+
'manage-contact': ['delete'],
|
|
205
|
+
'mailbox-settings': ['set-auto-replies'],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Classes that make a tool destructive and need a human's confirmation. */
|
|
209
|
+
const HIGH_RISK = new Set(['outward', 'destructive', 'persistent']);
|
|
210
|
+
/** Classes whose effects reach people outside the mailbox. */
|
|
211
|
+
const REACHES_OTHERS = new Set(['outward', 'persistent']);
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Every class a tool can take, across all its actions.
|
|
215
|
+
* @param {object} entry - TOOL_RISK entry
|
|
216
|
+
* @returns {string[]}
|
|
217
|
+
*/
|
|
218
|
+
function classesOf(entry) {
|
|
219
|
+
return entry.actions ? Object.values(entry.actions) : [entry.default];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Risk class for a tool call.
|
|
224
|
+
* @param {string} toolName
|
|
225
|
+
* @param {string|null} [action] - the call's `action` argument, for action
|
|
226
|
+
* tools; when left out or null (handlers treat both alike), the tool's
|
|
227
|
+
* defaultAction (if any) is classified
|
|
228
|
+
* @returns {string|undefined} the class, or undefined if unclassified
|
|
229
|
+
*/
|
|
230
|
+
function classify(toolName, action) {
|
|
231
|
+
const entry = TOOL_RISK[toolName];
|
|
232
|
+
if (!entry) return undefined;
|
|
233
|
+
if (!entry.actions) return entry.default;
|
|
234
|
+
const effective = effectiveAction(toolName, action);
|
|
235
|
+
return Object.hasOwn(entry.actions, effective)
|
|
236
|
+
? entry.actions[effective]
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The action a call runs: its `action` argument, or the tool's defaultAction
|
|
242
|
+
* when that is left out or null (handlers treat both alike).
|
|
243
|
+
* @param {string} toolName
|
|
244
|
+
* @param {string|null} [action]
|
|
245
|
+
* @returns {string|undefined}
|
|
246
|
+
*/
|
|
247
|
+
function effectiveAction(toolName, action) {
|
|
248
|
+
return action ?? TOOL_RISK[toolName]?.defaultAction;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Whether a call honours `dryRun: true` (see DRY_RUN_ACTIONS). Resolves a
|
|
253
|
+
* missing or null action to the tool's defaultAction, like classify().
|
|
254
|
+
* @param {string} toolName
|
|
255
|
+
* @param {string|null} [action]
|
|
256
|
+
* @returns {boolean}
|
|
257
|
+
*/
|
|
258
|
+
function supportsDryRun(toolName, action) {
|
|
259
|
+
const supported = DRY_RUN_ACTIONS[toolName];
|
|
260
|
+
if (supported === true) return true;
|
|
261
|
+
if (!Array.isArray(supported)) return false;
|
|
262
|
+
return supported.includes(effectiveAction(toolName, action));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* MCP annotations for a tool, derived from its risk classes (#277). All four
|
|
267
|
+
* hints are always set explicitly.
|
|
268
|
+
* @param {string} toolName
|
|
269
|
+
* @param {string} title - display title
|
|
270
|
+
* @returns {{title: string, readOnlyHint: boolean, destructiveHint: boolean, idempotentHint: boolean, openWorldHint: boolean}}
|
|
271
|
+
*/
|
|
272
|
+
function riskAnnotations(toolName, title) {
|
|
273
|
+
const entry = TOOL_RISK[toolName];
|
|
274
|
+
if (!entry) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Tool '${toolName}' has no risk class; add it to utils/risk-classes.js`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const classes = classesOf(entry);
|
|
280
|
+
const readOnly = classes.every((c) => c === 'read');
|
|
281
|
+
return {
|
|
282
|
+
title,
|
|
283
|
+
readOnlyHint: readOnly,
|
|
284
|
+
destructiveHint: classes.some((c) => HIGH_RISK.has(c)),
|
|
285
|
+
idempotentHint: readOnly || entry.idempotent === true,
|
|
286
|
+
openWorldHint:
|
|
287
|
+
entry.untrustedContent === true ||
|
|
288
|
+
classes.some((c) => REACHES_OTHERS.has(c)),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Top-level tool metadata: the spec's `title` plus derived annotations.
|
|
294
|
+
* Spread into a tool definition: `{ name, ...toolMetadata(name, title), ... }`.
|
|
295
|
+
* @param {string} toolName
|
|
296
|
+
* @param {string} title
|
|
297
|
+
* @returns {{title: string, annotations: object}}
|
|
298
|
+
*/
|
|
299
|
+
function toolMetadata(toolName, title) {
|
|
300
|
+
return { title, annotations: riskAnnotations(toolName, title) };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Tool-level `_meta` for tools/list, derived from the map (#271). Currently
|
|
305
|
+
* only Claude's `anthropic/requiresUserInteraction`; other clients ignore it.
|
|
306
|
+
* @param {string} toolName
|
|
307
|
+
* @returns {object|undefined} the `_meta` object, or undefined for none
|
|
308
|
+
*/
|
|
309
|
+
function riskMeta(toolName) {
|
|
310
|
+
const entry = TOOL_RISK[toolName];
|
|
311
|
+
if (!entry || entry.requiresUserInteraction !== true) return undefined;
|
|
312
|
+
return { 'anthropic/requiresUserInteraction': true };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
DRY_RUN_ACTIONS,
|
|
317
|
+
RISK_CLASSES,
|
|
318
|
+
TOOL_RISK,
|
|
319
|
+
classify,
|
|
320
|
+
riskAnnotations,
|
|
321
|
+
riskMeta,
|
|
322
|
+
supportsDryRun,
|
|
323
|
+
toolMetadata,
|
|
324
|
+
};
|