@holon-run/agentinbox 1.7.2 → 1.8.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/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 +2 -1
- package/dist/src/sources/email.js +49 -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
|
{
|
|
@@ -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,7 @@ 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);
|
|
101
107
|
return {
|
|
102
108
|
provider: provider,
|
|
103
109
|
endpoint,
|
|
@@ -109,6 +115,23 @@ function parseEmailMailboxSourceConfig(source) {
|
|
|
109
115
|
...(smtpEndpoint ? { smtpEndpoint } : {}),
|
|
110
116
|
...(fromAddress ? { fromAddress } : {}),
|
|
111
117
|
...(addressAllowlist && addressAllowlist.length > 0 ? { addressAllowlist } : {}),
|
|
118
|
+
attachmentPolicy,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function parseEmailAttachmentPolicy(value) {
|
|
122
|
+
const input = asRecord(value);
|
|
123
|
+
const mode = asString(input.mode) ?? "metadata";
|
|
124
|
+
if (mode !== "metadata" && mode !== "store_reference") {
|
|
125
|
+
throw new Error(`email_mailbox config.attachmentPolicy.mode must be metadata or store_reference; got ${mode}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
mode,
|
|
129
|
+
maxAttachmentsPerMessage: positiveIntegerOrDefault(input.maxAttachmentsPerMessage ?? input.max_attachments_per_message, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_COUNT, "maxAttachmentsPerMessage"),
|
|
130
|
+
maxBytesPerAttachment: positiveIntegerOrDefault(input.maxBytesPerAttachment ?? input.max_bytes_per_attachment, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_BYTES, "maxBytesPerAttachment"),
|
|
131
|
+
maxBytesPerMessage: positiveIntegerOrDefault(input.maxBytesPerMessage ?? input.max_bytes_per_message, exports.EMAIL_ATTACHMENT_DEFAULT_MAX_MESSAGE_BYTES, "maxBytesPerMessage"),
|
|
132
|
+
allowContentTypes: normalizedContentTypes(input.allowContentTypes ?? input.allow_content_types),
|
|
133
|
+
denyContentTypes: normalizedContentTypes(input.denyContentTypes ?? input.deny_content_types),
|
|
134
|
+
retentionSecs: positiveIntegerOrDefault(input.retentionSecs ?? input.retention_secs, exports.EMAIL_ATTACHMENT_DEFAULT_RETENTION_SECS, "retentionSecs"),
|
|
112
135
|
};
|
|
113
136
|
}
|
|
114
137
|
function buildEmailMailboxSourceSpec(config) {
|
|
@@ -232,6 +255,7 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
232
255
|
const mailbox = asString(payload.mailbox) ?? config.mailbox;
|
|
233
256
|
const uid = stringFromUnknown(message.uid);
|
|
234
257
|
const messageId = asString(message.message_id) ?? uid;
|
|
258
|
+
const stableKey = asString(message.stable_key);
|
|
235
259
|
if (!uid && !messageId) {
|
|
236
260
|
return null;
|
|
237
261
|
}
|
|
@@ -247,10 +271,18 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
247
271
|
: [];
|
|
248
272
|
const attachmentCountRaw = message.attachment_count;
|
|
249
273
|
const attachmentCount = numberFromUnknown(attachmentCountRaw) ?? null;
|
|
274
|
+
const hasAttachments = attachments.length > 0
|
|
275
|
+
|| (attachmentCount !== null && attachmentCount > 0)
|
|
276
|
+
|| message.has_attachments === true;
|
|
277
|
+
const attachmentsComplete = attachmentCount !== null
|
|
278
|
+
? attachmentCount === attachments.length
|
|
279
|
+
: message.has_attachments === false && attachments.length === 0;
|
|
250
280
|
const date = asString(message.date);
|
|
251
281
|
return {
|
|
252
282
|
sourceId: source.sourceId,
|
|
253
|
-
sourceNativeId:
|
|
283
|
+
sourceNativeId: stableKey
|
|
284
|
+
? `email:${stableKey}`
|
|
285
|
+
: `email:${provider}:${account}:${mailbox}:${uid ?? messageId}`,
|
|
254
286
|
eventVariant: "email.message.received",
|
|
255
287
|
occurredAt: occurredAtFromEmailDate(date),
|
|
256
288
|
metadata: {
|
|
@@ -267,8 +299,9 @@ function normalizeEmailMailboxEvent(source, config, raw) {
|
|
|
267
299
|
subject: asString(message.subject),
|
|
268
300
|
textPreview: asString(message.snippet),
|
|
269
301
|
date,
|
|
270
|
-
hasAttachments
|
|
302
|
+
hasAttachments,
|
|
271
303
|
attachmentCount,
|
|
304
|
+
attachmentsComplete,
|
|
272
305
|
attachments,
|
|
273
306
|
},
|
|
274
307
|
rawPayload: payload,
|
|
@@ -552,3 +585,16 @@ function numberFromUnknown(value) {
|
|
|
552
585
|
}
|
|
553
586
|
return undefined;
|
|
554
587
|
}
|
|
588
|
+
function positiveIntegerOrDefault(value, fallback, field) {
|
|
589
|
+
if (value === undefined || value === null) {
|
|
590
|
+
return fallback;
|
|
591
|
+
}
|
|
592
|
+
const parsed = numberFromUnknown(value);
|
|
593
|
+
if (parsed === undefined || !Number.isSafeInteger(parsed) || parsed < 1) {
|
|
594
|
+
throw new Error(`email_mailbox config.attachmentPolicy.${field} must be a positive integer; got ${String(value)}`);
|
|
595
|
+
}
|
|
596
|
+
return parsed;
|
|
597
|
+
}
|
|
598
|
+
function normalizedContentTypes(value) {
|
|
599
|
+
return [...new Set((asStringArray(value) ?? []).map((item) => item.trim().toLowerCase()))];
|
|
600
|
+
}
|
|
@@ -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
|
};
|