@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.
@@ -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
- constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate, logger = new logging_1.NoopLogger(), gatingPolicy) {
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: "attachments", type: "object[]", description: "Attachment metadata entries with opaque retrieval handles; binary content is never embedded." },
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: `email:${provider}:${account}:${mailbox}:${uid ?? messageId}`,
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: attachments.length > 0 || message.has_attachments === true,
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: "attachments", type: "object[]", description: "Attachment metadata with opaque retrieval handles." },
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
  };