@holon-run/agentinbox 1.7.2 → 1.8.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/README.md +22 -0
- package/dist/src/cli.js +150 -8
- package/dist/src/client.js +52 -0
- package/dist/src/email_attachment.js +199 -0
- package/dist/src/email_attachment_content.js +759 -0
- package/dist/src/email_body.js +392 -0
- package/dist/src/http.js +147 -3
- package/dist/src/service.js +61 -3
- package/dist/src/source_schema.js +4 -1
- package/dist/src/sources/email.js +72 -3
- package/dist/src/sources/remote_modules.js +2 -1
- package/dist/src/store.js +451 -1
- package/drizzle/migrations/0006_email_body_cache.sql +19 -0
- package/drizzle/migrations/0007_email_attachment_materializations.sql +20 -0
- package/drizzle/migrations/0008_email_attachment_audit.sql +17 -0
- package/drizzle/schema.ts +62 -0
- package/package.json +2 -2
- package/skills/agentinbox/SKILL.md +20 -0
package/dist/src/service.js
CHANGED
|
@@ -11,6 +11,9 @@ const terminal_1 = require("./terminal");
|
|
|
11
11
|
const runtime_gate_1 = require("./runtime_gate");
|
|
12
12
|
const logging_1 = require("./logging");
|
|
13
13
|
const source_hosts_1 = require("./source_hosts");
|
|
14
|
+
const email_body_1 = require("./email_body");
|
|
15
|
+
const email_attachment_1 = require("./email_attachment");
|
|
16
|
+
const email_attachment_content_1 = require("./email_attachment_content");
|
|
14
17
|
const DEFAULT_SUBSCRIPTION_POLL_LIMIT = 100;
|
|
15
18
|
const DEFAULT_ACTIVATION_WINDOW_MS = 3_000;
|
|
16
19
|
const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
|
|
@@ -51,7 +54,9 @@ class AgentInboxService {
|
|
|
51
54
|
lastAckedInboxGcAt = 0;
|
|
52
55
|
lastLifecycleCleanupAt = 0;
|
|
53
56
|
lastOfflineAgentGcAt = 0;
|
|
54
|
-
|
|
57
|
+
emailBodyReader;
|
|
58
|
+
emailAttachmentContent;
|
|
59
|
+
constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate, logger = new logging_1.NoopLogger(), gatingPolicy, emailBodyUxcClient, emailAttachmentOptions) {
|
|
55
60
|
this.store = store;
|
|
56
61
|
this.adapters = adapters;
|
|
57
62
|
this.activationDispatcher = activationDispatcher;
|
|
@@ -62,6 +67,8 @@ class AgentInboxService {
|
|
|
62
67
|
this.terminalDispatcher = terminalDispatcher;
|
|
63
68
|
this.logger = logger;
|
|
64
69
|
this.activationGate = activationGate ?? new runtime_gate_1.DefaultActivationGate(undefined, undefined, this.logger.child("gate"), gatingPolicy);
|
|
70
|
+
this.emailBodyReader = new email_body_1.EmailBodyReader(store, emailBodyUxcClient);
|
|
71
|
+
this.emailAttachmentContent = new email_attachment_content_1.EmailAttachmentContentManager(store, `${store.getDataDirectory()}/attachments`, emailBodyUxcClient, emailAttachmentOptions);
|
|
65
72
|
}
|
|
66
73
|
activationDispatcher;
|
|
67
74
|
terminalDispatcher;
|
|
@@ -634,6 +641,7 @@ class AgentInboxService {
|
|
|
634
641
|
this.getAgent(agentId);
|
|
635
642
|
const affectedSourceIds = Array.from(new Set(this.store.listSubscriptionsForAgent(agentId).map((subscription) => subscription.sourceId)));
|
|
636
643
|
this.store.deleteAgent(agentId);
|
|
644
|
+
this.emailAttachmentContent.gc();
|
|
637
645
|
for (const sourceId of affectedSourceIds) {
|
|
638
646
|
this.refreshSourceIdleState(sourceId);
|
|
639
647
|
}
|
|
@@ -1149,6 +1157,51 @@ class AgentInboxService {
|
|
|
1149
1157
|
listInboxItems(agentId, options) {
|
|
1150
1158
|
return this.store.listInboxEntries(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
1151
1159
|
}
|
|
1160
|
+
async readInboxEmailBody(agentId, entryId, options) {
|
|
1161
|
+
const inbox = this.store.getInboxByAgentId(agentId);
|
|
1162
|
+
if (!inbox) {
|
|
1163
|
+
throw new Error(`unknown inbox entry: ${entryId}`);
|
|
1164
|
+
}
|
|
1165
|
+
const entry = this.store.getInboxEntryForInbox(inbox.inboxId, entryId);
|
|
1166
|
+
if (!entry) {
|
|
1167
|
+
throw new Error(`unknown inbox entry: ${entryId}`);
|
|
1168
|
+
}
|
|
1169
|
+
if (entry.kind !== "item") {
|
|
1170
|
+
return {
|
|
1171
|
+
status: "unavailable",
|
|
1172
|
+
entryId: entry.entryId,
|
|
1173
|
+
code: "unsupported",
|
|
1174
|
+
retryable: false,
|
|
1175
|
+
message: "Digest entries do not represent one email body.",
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const source = entry.sourceId ? this.store.getSource(entry.sourceId) : null;
|
|
1179
|
+
if (!source) {
|
|
1180
|
+
return {
|
|
1181
|
+
status: "unavailable",
|
|
1182
|
+
entryId: entry.entryId,
|
|
1183
|
+
code: "source_unavailable",
|
|
1184
|
+
retryable: false,
|
|
1185
|
+
message: "The source for this inbox entry is no longer available.",
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return this.emailBodyReader.read(inbox.inboxId, entry, source, options);
|
|
1189
|
+
}
|
|
1190
|
+
getInboxEmailAttachment(agentId, attachmentRef) {
|
|
1191
|
+
return this.emailAttachmentContent.inspect(agentId, attachmentRef);
|
|
1192
|
+
}
|
|
1193
|
+
async materializeInboxEmailAttachment(agentId, attachmentRef) {
|
|
1194
|
+
return this.emailAttachmentContent.materialize(agentId, attachmentRef);
|
|
1195
|
+
}
|
|
1196
|
+
openInboxEmailAttachmentContent(agentId, attachmentRef) {
|
|
1197
|
+
return this.emailAttachmentContent.openContent(agentId, attachmentRef);
|
|
1198
|
+
}
|
|
1199
|
+
deleteInboxEmailAttachmentContent(agentId, attachmentRef) {
|
|
1200
|
+
return this.emailAttachmentContent.delete(agentId, attachmentRef);
|
|
1201
|
+
}
|
|
1202
|
+
projectPublicInboxEntry(value) {
|
|
1203
|
+
return (0, email_attachment_1.projectPublicInboxEntry)(value, (itemId, selector) => this.store.getEmailAttachmentMaterialization(itemId, selector));
|
|
1204
|
+
}
|
|
1152
1205
|
listRawInboxItems(agentId, options) {
|
|
1153
1206
|
return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
1154
1207
|
}
|
|
@@ -1462,12 +1515,14 @@ class AgentInboxService {
|
|
|
1462
1515
|
return {
|
|
1463
1516
|
deleted: this.store.deleteAckedInboxItems(inbox.inboxId, retentionCutoffIso(this.ackedRetentionMs)),
|
|
1464
1517
|
retentionMs: this.ackedRetentionMs,
|
|
1518
|
+
attachments: this.emailAttachmentContent.gc(),
|
|
1465
1519
|
};
|
|
1466
1520
|
}
|
|
1467
1521
|
gcAckedInboxItems() {
|
|
1468
1522
|
return {
|
|
1469
1523
|
deleted: this.store.deleteAckedInboxItemsGlobal(retentionCutoffIso(this.ackedRetentionMs)),
|
|
1470
1524
|
retentionMs: this.ackedRetentionMs,
|
|
1525
|
+
attachments: this.emailAttachmentContent.gc(),
|
|
1471
1526
|
};
|
|
1472
1527
|
}
|
|
1473
1528
|
gc() {
|
|
@@ -2676,9 +2731,11 @@ class AgentInboxService {
|
|
|
2676
2731
|
items: target.mode === "activation_with_items"
|
|
2677
2732
|
? input.entries
|
|
2678
2733
|
.filter((entry) => entry.kind === "item")
|
|
2679
|
-
.map((entry) => entry.item)
|
|
2734
|
+
.map((entry) => this.projectPublicInboxEntry(entry.item))
|
|
2735
|
+
: undefined,
|
|
2736
|
+
entries: target.mode === "activation_with_items" && input.entries.length > 0
|
|
2737
|
+
? input.entries.map((entry) => this.projectPublicInboxEntry(entry))
|
|
2680
2738
|
: undefined,
|
|
2681
|
-
entries: target.mode === "activation_with_items" && input.entries.length > 0 ? input.entries : undefined,
|
|
2682
2739
|
createdAt: (0, util_1.nowIso)(),
|
|
2683
2740
|
deliveredAt: null,
|
|
2684
2741
|
};
|
|
@@ -3075,6 +3132,7 @@ class AgentInboxService {
|
|
|
3075
3132
|
}
|
|
3076
3133
|
if (agents.length > 0) {
|
|
3077
3134
|
this.store.save();
|
|
3135
|
+
this.emailAttachmentContent.gc();
|
|
3078
3136
|
}
|
|
3079
3137
|
if (agents.length > 0) {
|
|
3080
3138
|
this.rescheduleTimerSync();
|
|
@@ -207,7 +207,8 @@ const SOURCE_SCHEMAS = {
|
|
|
207
207
|
{ name: "date", type: "string|null", description: "Message date header value." },
|
|
208
208
|
{ name: "hasAttachments", type: "boolean", description: "Whether the message carries attachments." },
|
|
209
209
|
{ name: "attachmentCount", type: "number|null", description: "Attachment count; null when the provider did not expand attachment metadata." },
|
|
210
|
-
{ name: "
|
|
210
|
+
{ name: "attachmentsComplete", type: "boolean", description: "Whether the attachment metadata list is known to be complete." },
|
|
211
|
+
{ name: "attachments", type: "object[]", description: "Safe attachment metadata with an opaque attachmentRef; provider retrieval handles and binary content are never exposed." },
|
|
211
212
|
],
|
|
212
213
|
payloadExamples: [
|
|
213
214
|
{
|
|
@@ -236,6 +237,8 @@ const SOURCE_SCHEMAS = {
|
|
|
236
237
|
{ name: "mailbox", type: "string", required: false, description: "Mailbox to watch; defaults to INBOX." },
|
|
237
238
|
{ name: "pollIntervalSecs", type: "number", required: false, description: "Provider poll interval in seconds (min 15, default 60)." },
|
|
238
239
|
{ name: "initialFetchLimit", type: "number", required: false, description: "First-look backfill depth: 0 = new mail only, max 100, default 25." },
|
|
240
|
+
{ name: "method", type: "string", required: false, description: "Provider poll HTTP method: get (default) or post; ignored for imap." },
|
|
241
|
+
{ name: "requestBody", type: "object", required: false, description: "JSON request body for provider polling; requires method=post and is ignored for imap." },
|
|
239
242
|
{ name: "smtpEndpoint", type: "string", required: false, description: "Default smtp:// endpoint for outbound reply/send delivery." },
|
|
240
243
|
{ name: "fromAddress", type: "string", required: false, description: "Default outbound From address for delivery." },
|
|
241
244
|
{ name: "addressAllowlist", type: "string[]", required: false, description: "Optional from/to address allowlist applied before inbox routing." },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EmailDeliveryAdapter = exports.RpcEmailUxcClient = exports.GRAPH_MESSAGES_ENDPOINT = exports.GMAIL_MESSAGES_ENDPOINT = exports.EMAIL_EVENT_VERSION = exports.EMAIL_EVENT_TYPE = exports.EMAIL_MAILBOX_MAX_INITIAL_FETCH_LIMIT = exports.EMAIL_MAILBOX_DEFAULT_INITIAL_FETCH_LIMIT = exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS = exports.EMAIL_DEFAULT_MAILBOX = void 0;
|
|
3
|
+
exports.EmailDeliveryAdapter = exports.RpcEmailUxcClient = exports.GRAPH_MESSAGES_ENDPOINT = exports.GMAIL_MESSAGES_ENDPOINT = exports.EMAIL_EVENT_VERSION = exports.EMAIL_EVENT_TYPE = exports.EMAIL_ATTACHMENT_DEFAULT_RETENTION_SECS = exports.EMAIL_ATTACHMENT_DEFAULT_MAX_MESSAGE_BYTES = exports.EMAIL_ATTACHMENT_DEFAULT_MAX_BYTES = exports.EMAIL_ATTACHMENT_DEFAULT_MAX_COUNT = exports.EMAIL_MAILBOX_MAX_INITIAL_FETCH_LIMIT = exports.EMAIL_MAILBOX_DEFAULT_INITIAL_FETCH_LIMIT = exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS = exports.EMAIL_DEFAULT_MAILBOX = void 0;
|
|
4
4
|
exports.parseEmailMailboxSourceConfig = parseEmailMailboxSourceConfig;
|
|
5
|
+
exports.parseEmailAttachmentPolicy = parseEmailAttachmentPolicy;
|
|
5
6
|
exports.buildEmailMailboxSourceSpec = buildEmailMailboxSourceSpec;
|
|
6
7
|
exports.normalizeEmailMailboxEvent = normalizeEmailMailboxEvent;
|
|
7
8
|
exports.emailDeliveryOperationsForHandle = emailDeliveryOperationsForHandle;
|
|
@@ -11,6 +12,10 @@ exports.EMAIL_DEFAULT_MAILBOX = "INBOX";
|
|
|
11
12
|
exports.EMAIL_MAILBOX_DEFAULT_POLL_INTERVAL_SECS = 60;
|
|
12
13
|
exports.EMAIL_MAILBOX_DEFAULT_INITIAL_FETCH_LIMIT = 25;
|
|
13
14
|
exports.EMAIL_MAILBOX_MAX_INITIAL_FETCH_LIMIT = 100;
|
|
15
|
+
exports.EMAIL_ATTACHMENT_DEFAULT_MAX_COUNT = 20;
|
|
16
|
+
exports.EMAIL_ATTACHMENT_DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
|
|
17
|
+
exports.EMAIL_ATTACHMENT_DEFAULT_MAX_MESSAGE_BYTES = 50 * 1024 * 1024;
|
|
18
|
+
exports.EMAIL_ATTACHMENT_DEFAULT_RETENTION_SECS = 24 * 60 * 60;
|
|
14
19
|
exports.EMAIL_EVENT_TYPE = "email_event";
|
|
15
20
|
exports.EMAIL_EVENT_VERSION = "v1";
|
|
16
21
|
exports.GMAIL_MESSAGES_ENDPOINT = "https://gmail.googleapis.com/gmail/v1/users/me/messages";
|
|
@@ -98,6 +103,26 @@ function parseEmailMailboxSourceConfig(source) {
|
|
|
98
103
|
throw new Error("email_mailbox config.mailbox must be a non-empty string when provided");
|
|
99
104
|
}
|
|
100
105
|
const addressAllowlist = asStringArray(config.addressAllowlist ?? config.addressAllowList);
|
|
106
|
+
const attachmentPolicy = parseEmailAttachmentPolicy(config.attachmentPolicy ?? config.attachment_policy);
|
|
107
|
+
const methodRaw = config.method;
|
|
108
|
+
let method = "get";
|
|
109
|
+
if (methodRaw !== undefined && methodRaw !== null) {
|
|
110
|
+
if (typeof methodRaw !== "string" || !["get", "post"].includes(methodRaw.trim().toLowerCase())) {
|
|
111
|
+
throw new Error(`email_mailbox config.method must be get or post; got ${String(methodRaw)}`);
|
|
112
|
+
}
|
|
113
|
+
method = methodRaw.trim().toLowerCase();
|
|
114
|
+
}
|
|
115
|
+
const requestBodyRaw = config.requestBody;
|
|
116
|
+
let requestBody;
|
|
117
|
+
if (requestBodyRaw !== undefined && requestBodyRaw !== null) {
|
|
118
|
+
if (typeof requestBodyRaw !== "object" || Array.isArray(requestBodyRaw)) {
|
|
119
|
+
throw new Error("email_mailbox config.requestBody must be a JSON object when provided");
|
|
120
|
+
}
|
|
121
|
+
if (method !== "post") {
|
|
122
|
+
throw new Error("email_mailbox config.requestBody requires config.method=post");
|
|
123
|
+
}
|
|
124
|
+
requestBody = requestBodyRaw;
|
|
125
|
+
}
|
|
101
126
|
return {
|
|
102
127
|
provider: provider,
|
|
103
128
|
endpoint,
|
|
@@ -106,9 +131,28 @@ function parseEmailMailboxSourceConfig(source) {
|
|
|
106
131
|
mailbox,
|
|
107
132
|
pollIntervalSecs,
|
|
108
133
|
initialFetchLimit,
|
|
134
|
+
method,
|
|
135
|
+
...(requestBody ? { requestBody } : {}),
|
|
109
136
|
...(smtpEndpoint ? { smtpEndpoint } : {}),
|
|
110
137
|
...(fromAddress ? { fromAddress } : {}),
|
|
111
138
|
...(addressAllowlist && addressAllowlist.length > 0 ? { addressAllowlist } : {}),
|
|
139
|
+
attachmentPolicy,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function parseEmailAttachmentPolicy(value) {
|
|
143
|
+
const input = asRecord(value);
|
|
144
|
+
const mode = asString(input.mode) ?? "metadata";
|
|
145
|
+
if (mode !== "metadata" && mode !== "store_reference") {
|
|
146
|
+
throw new Error(`email_mailbox config.attachmentPolicy.mode must be metadata or store_reference; got ${mode}`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
mode,
|
|
150
|
+
maxAttachmentsPerMessage: positiveIntegerOrDefault(input.maxAttachmentsPerMessage ?? input.max_attachments_per_message, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_COUNT, "maxAttachmentsPerMessage"),
|
|
151
|
+
maxBytesPerAttachment: positiveIntegerOrDefault(input.maxBytesPerAttachment ?? input.max_bytes_per_attachment, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_BYTES, "maxBytesPerAttachment"),
|
|
152
|
+
maxBytesPerMessage: positiveIntegerOrDefault(input.maxBytesPerMessage ?? input.max_bytes_per_message, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_MESSAGE_BYTES, "maxBytesPerMessage"),
|
|
153
|
+
allowContentTypes: normalizedContentTypes(input.allowContentTypes ?? input.allow_content_types),
|
|
154
|
+
denyContentTypes: normalizedContentTypes(input.denyContentTypes ?? input.deny_content_types),
|
|
155
|
+
retentionSecs: positiveIntegerOrDefault(input.retentionSecs ?? input.retention_secs, exports.EMAIL_ATTACHMENT_DEFAULT_RETENTION_SECS, "retentionSecs"),
|
|
112
156
|
};
|
|
113
157
|
}
|
|
114
158
|
function buildEmailMailboxSourceSpec(config) {
|
|
@@ -148,7 +192,9 @@ function buildEmailMailboxSourceSpec(config) {
|
|
|
148
192
|
transport_hint: "email_provider_poll",
|
|
149
193
|
args: {
|
|
150
194
|
provider: config.provider,
|
|
195
|
+
method: config.method,
|
|
151
196
|
...args,
|
|
197
|
+
...(config.requestBody ? { body: config.requestBody } : {}),
|
|
152
198
|
},
|
|
153
199
|
poll_config: pollConfig,
|
|
154
200
|
options: {
|
|
@@ -232,6 +278,7 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
232
278
|
const mailbox = asString(payload.mailbox) ?? config.mailbox;
|
|
233
279
|
const uid = stringFromUnknown(message.uid);
|
|
234
280
|
const messageId = asString(message.message_id) ?? uid;
|
|
281
|
+
const stableKey = asString(message.stable_key);
|
|
235
282
|
if (!uid && !messageId) {
|
|
236
283
|
return null;
|
|
237
284
|
}
|
|
@@ -247,10 +294,18 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
247
294
|
: [];
|
|
248
295
|
const attachmentCountRaw = message.attachment_count;
|
|
249
296
|
const attachmentCount = numberFromUnknown(attachmentCountRaw) ?? null;
|
|
297
|
+
const hasAttachments = attachments.length > 0
|
|
298
|
+
|| (attachmentCount !== null && attachmentCount > 0)
|
|
299
|
+
|| message.has_attachments === true;
|
|
300
|
+
const attachmentsComplete = attachmentCount !== null
|
|
301
|
+
? attachmentCount === attachments.length
|
|
302
|
+
: message.has_attachments === false && attachments.length === 0;
|
|
250
303
|
const date = asString(message.date);
|
|
251
304
|
return {
|
|
252
305
|
sourceId: source.sourceId,
|
|
253
|
-
sourceNativeId:
|
|
306
|
+
sourceNativeId: stableKey
|
|
307
|
+
? `email:${stableKey}`
|
|
308
|
+
: `email:${provider}:${account}:${mailbox}:${uid ?? messageId}`,
|
|
254
309
|
eventVariant: "email.message.received",
|
|
255
310
|
occurredAt: occurredAtFromEmailDate(date),
|
|
256
311
|
metadata: {
|
|
@@ -267,8 +322,9 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
267
322
|
subject: asString(message.subject),
|
|
268
323
|
textPreview: asString(message.snippet),
|
|
269
324
|
date,
|
|
270
|
-
hasAttachments
|
|
325
|
+
hasAttachments,
|
|
271
326
|
attachmentCount,
|
|
327
|
+
attachmentsComplete,
|
|
272
328
|
attachments,
|
|
273
329
|
},
|
|
274
330
|
rawPayload: payload,
|
|
@@ -552,3 +608,16 @@ function numberFromUnknown(value) {
|
|
|
552
608
|
}
|
|
553
609
|
return undefined;
|
|
554
610
|
}
|
|
611
|
+
function positiveIntegerOrDefault(value, fallback, field) {
|
|
612
|
+
if (value === undefined || value === null) {
|
|
613
|
+
return fallback;
|
|
614
|
+
}
|
|
615
|
+
const parsed = numberFromUnknown(value);
|
|
616
|
+
if (parsed === undefined || !Number.isSafeInteger(parsed) || parsed < 1) {
|
|
617
|
+
throw new Error(`email_mailbox config.attachmentPolicy.${field} must be a positive integer; got ${String(value)}`);
|
|
618
|
+
}
|
|
619
|
+
return parsed;
|
|
620
|
+
}
|
|
621
|
+
function normalizedContentTypes(value) {
|
|
622
|
+
return [...new Set((asStringArray(value) ?? []).map((item) => item.trim().toLowerCase()))];
|
|
623
|
+
}
|
|
@@ -680,7 +680,8 @@ const EMAIL_MAILBOX_MODULE = {
|
|
|
680
680
|
{ name: "textPreview", type: "string|null", description: "Body snippet." },
|
|
681
681
|
{ name: "hasAttachments", type: "boolean", description: "Whether the message carries attachments." },
|
|
682
682
|
{ name: "attachmentCount", type: "number|null", description: "Attachment count; null when provider metadata was not expanded." },
|
|
683
|
-
{ name: "
|
|
683
|
+
{ name: "attachmentsComplete", type: "boolean", description: "Whether the attachment metadata list is known to be complete." },
|
|
684
|
+
{ name: "attachments", type: "object[]", description: "Safe attachment metadata with an opaque attachmentRef; provider retrieval handles are not exposed." },
|
|
684
685
|
],
|
|
685
686
|
eventVariantExamples: ["email.message.received"],
|
|
686
687
|
};
|