@ouro.bot/cli 0.1.0-alpha.485 → 0.1.0-alpha.488
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/changelog.json +15 -0
- package/dist/heart/active-work.js +89 -3
- package/dist/heart/background-operations.js +26 -3
- package/dist/heart/daemon/cli-exec.js +171 -9
- package/dist/heart/mail-import-discovery.js +37 -2
- package/dist/heart/providers/azure.js +1 -1
- package/dist/heart/providers/github-copilot.js +1 -1
- package/dist/heart/providers/openai-codex.js +1 -1
- package/dist/heart/streaming.js +13 -2
- package/dist/mailroom/blob-store.js +16 -10
- package/dist/mailroom/core.js +1 -1
- package/dist/mailroom/file-store.js +35 -9
- package/dist/mailroom/mbox-import.js +41 -0
- package/dist/mailroom/reader.js +22 -0
- package/dist/mailroom/search-cache.js +182 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/nerves/coverage/file-completeness.js +4 -0
- package/dist/repertoire/tools-mail.js +453 -68
- package/dist/senses/bluebubbles/inbound-log.js +13 -0
- package/dist/senses/bluebubbles/index.js +406 -237
- package/dist/senses/bluebubbles/processed-log.js +111 -0
- package/dist/senses/mail.js +19 -3
- package/package.json +1 -1
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
35
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.mailToolDefinitions = void 0;
|
|
37
|
-
|
|
6
|
+
exports.mailToolDefinitions = exports.__mailStatusTestOnly = void 0;
|
|
7
|
+
exports.renderCachedMessageSummary = renderCachedMessageSummary;
|
|
8
|
+
exports.mergeCachedMailSearchDocuments = mergeCachedMailSearchDocuments;
|
|
9
|
+
exports.searchSuccessfulImportArchives = searchSuccessfulImportArchives;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
38
11
|
const types_1 = require("../mind/friends/types");
|
|
39
12
|
const file_store_1 = require("../mailroom/file-store");
|
|
40
13
|
const reader_1 = require("../mailroom/reader");
|
|
41
14
|
const outbound_1 = require("../mailroom/outbound");
|
|
42
15
|
const policy_1 = require("../mailroom/policy");
|
|
16
|
+
const search_cache_1 = require("../mailroom/search-cache");
|
|
17
|
+
const mbox_import_1 = require("../mailroom/mbox-import");
|
|
18
|
+
const search_relevance_1 = require("../mailroom/search-relevance");
|
|
43
19
|
const core_1 = require("../mailroom/core");
|
|
44
20
|
const runtime_1 = require("../nerves/runtime");
|
|
45
21
|
const credential_access_1 = require("./credential-access");
|
|
22
|
+
const background_operations_1 = require("../heart/background-operations");
|
|
23
|
+
const mail_import_discovery_1 = require("../heart/mail-import-discovery");
|
|
24
|
+
const identity_1 = require("../heart/identity");
|
|
46
25
|
function trustAllowsMailRead(ctx) {
|
|
47
26
|
const trustLevel = ctx?.context?.friend?.trustLevel;
|
|
48
27
|
const allowed = trustLevel === undefined || (0, types_1.isTrustedLevel)(trustLevel);
|
|
@@ -92,6 +71,13 @@ function parseMailList(value) {
|
|
|
92
71
|
.map((entry) => entry.trim())
|
|
93
72
|
.filter(Boolean);
|
|
94
73
|
}
|
|
74
|
+
function mailSearchTerms(query) {
|
|
75
|
+
return query
|
|
76
|
+
.split(/\s+OR\s+/i)
|
|
77
|
+
.flatMap((entry) => entry.split(/[\n,;]+/))
|
|
78
|
+
.map((entry) => entry.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
}
|
|
95
81
|
function missingPrivateMailKeyId(error) {
|
|
96
82
|
const match = /^(?:Error: )?Missing private mail key ([^\s]+)$/.exec(String(error));
|
|
97
83
|
return match?.[1] ?? null;
|
|
@@ -183,6 +169,49 @@ function renderMessageSummary(message) {
|
|
|
183
169
|
` warning: ${message.private.untrustedContentWarning}`,
|
|
184
170
|
].join("\n");
|
|
185
171
|
}
|
|
172
|
+
function renderCachedMessageSummary(message, queryTerms = []) {
|
|
173
|
+
const scope = message.compartmentKind === "delegated"
|
|
174
|
+
? `delegated:${message.ownerEmail ?? "unknown"}:${message.source ?? "source"}`
|
|
175
|
+
: "native";
|
|
176
|
+
const from = message.from.join(", ") || "(unknown sender)";
|
|
177
|
+
const subject = message.subject || "(no subject)";
|
|
178
|
+
const lines = [
|
|
179
|
+
`- ${message.messageId} [${message.placement}; ${scope}]`,
|
|
180
|
+
` from: ${from}`,
|
|
181
|
+
` subject: ${subject}`,
|
|
182
|
+
];
|
|
183
|
+
if (typeof message.attachmentCount === "number" && message.attachmentCount > 0) {
|
|
184
|
+
lines.push(` attachments: ${message.attachmentCount}`);
|
|
185
|
+
}
|
|
186
|
+
if (queryTerms.length > 0) {
|
|
187
|
+
const hint = (0, search_relevance_1.formatRelevanceHint)((0, search_relevance_1.scoreMailSearchDocument)(message, queryTerms));
|
|
188
|
+
if (hint)
|
|
189
|
+
lines.push(` matched on: ${hint}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(` snippet: ${message.snippet}`);
|
|
192
|
+
lines.push(` warning: ${message.untrustedContentWarning}`);
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
function mergeCachedMailSearchDocuments(cached, imported, limit, queryTerms = []) {
|
|
196
|
+
const merged = [];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
const all = [...cached, ...imported];
|
|
199
|
+
const ordered = queryTerms.length > 0
|
|
200
|
+
? all
|
|
201
|
+
.map((document) => ({ document, relevance: (0, search_relevance_1.scoreMailSearchDocument)(document, queryTerms) }))
|
|
202
|
+
.sort(search_relevance_1.compareByRelevanceThenRecency)
|
|
203
|
+
.map((entry) => entry.document)
|
|
204
|
+
: all.sort((left, right) => right.receivedAt.localeCompare(left.receivedAt));
|
|
205
|
+
for (const message of ordered) {
|
|
206
|
+
if (seen.has(message.messageId))
|
|
207
|
+
continue;
|
|
208
|
+
seen.add(message.messageId);
|
|
209
|
+
merged.push(message);
|
|
210
|
+
if (merged.length >= limit)
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return merged;
|
|
214
|
+
}
|
|
186
215
|
function renderScreenerCandidate(candidate) {
|
|
187
216
|
const delegated = candidate.ownerEmail || candidate.source
|
|
188
217
|
? ` delegated:${candidate.ownerEmail ?? "unknown"}:${candidate.source ?? "source"}`
|
|
@@ -196,9 +225,12 @@ function renderScreenerCandidate(candidate) {
|
|
|
196
225
|
].join("\n");
|
|
197
226
|
}
|
|
198
227
|
function renderAccessLog(entries) {
|
|
228
|
+
const warning = typeof entries.malformedEntriesSkipped === "number" && entries.malformedEntriesSkipped > 0
|
|
229
|
+
? `warning: skipped ${entries.malformedEntriesSkipped} malformed file-backed mail access log line${entries.malformedEntriesSkipped === 1 ? "" : "s"}`
|
|
230
|
+
: "";
|
|
199
231
|
if (entries.length === 0)
|
|
200
|
-
return "No mail access records yet.";
|
|
201
|
-
|
|
232
|
+
return warning || "No mail access records yet.";
|
|
233
|
+
const rendered = entries
|
|
202
234
|
.slice(-20)
|
|
203
235
|
.reverse()
|
|
204
236
|
.map((entry) => {
|
|
@@ -207,6 +239,7 @@ function renderAccessLog(entries) {
|
|
|
207
239
|
return `- ${entry.accessedAt} ${entry.tool} ${target}${provenance} reason="${entry.reason}"`;
|
|
208
240
|
})
|
|
209
241
|
.join("\n");
|
|
242
|
+
return warning ? `${warning}\n${rendered}` : rendered;
|
|
210
243
|
}
|
|
211
244
|
function renderAccessLogProvenance(entry) {
|
|
212
245
|
if (entry.mailboxRole === "delegated-human-mailbox") {
|
|
@@ -217,6 +250,11 @@ function renderAccessLogProvenance(entry) {
|
|
|
217
250
|
}
|
|
218
251
|
return "";
|
|
219
252
|
}
|
|
253
|
+
function cacheDecryptedMessages(messages) {
|
|
254
|
+
for (const message of messages) {
|
|
255
|
+
(0, search_cache_1.upsertMailSearchCacheDocument)(message, message.private);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
220
258
|
function accessProvenance(message) {
|
|
221
259
|
const provenance = (0, core_1.describeMailProvenance)(message);
|
|
222
260
|
return {
|
|
@@ -226,15 +264,9 @@ function accessProvenance(message) {
|
|
|
226
264
|
source: provenance.source,
|
|
227
265
|
};
|
|
228
266
|
}
|
|
229
|
-
function renderSourceGrantStatus(config, agentId) {
|
|
230
|
-
if (!config.registryPath) {
|
|
231
|
-
return [
|
|
232
|
-
"delegated source aliases: unknown (runtime config has no registryPath).",
|
|
233
|
-
`agent-runnable repair: run ouro connect mail --agent ${agentId} --owner-email <human-email> --source hey.`,
|
|
234
|
-
];
|
|
235
|
-
}
|
|
267
|
+
async function renderSourceGrantStatus(config, agentId) {
|
|
236
268
|
try {
|
|
237
|
-
const registry =
|
|
269
|
+
const registry = await (0, reader_1.readMailroomRegistry)(config);
|
|
238
270
|
const grants = registry.sourceGrants
|
|
239
271
|
.filter((grant) => grant.agentId === agentId && grant.enabled)
|
|
240
272
|
.map((grant) => `${grant.source}:${grant.ownerEmail} -> ${grant.aliasAddress}`);
|
|
@@ -257,10 +289,11 @@ function renderSourceGrantStatus(config, agentId) {
|
|
|
257
289
|
async function renderEmptyMailResult(input) {
|
|
258
290
|
const anyVisible = await input.store.listMessages({ agentId: input.agentId, limit: 1 });
|
|
259
291
|
if (anyVisible.length === 0) {
|
|
292
|
+
const sourceGrantStatus = await renderSourceGrantStatus(input.config, input.agentId);
|
|
260
293
|
return [
|
|
261
294
|
"No visible mail yet.",
|
|
262
295
|
`mail onboarding status: Mailroom is provisioned for ${input.config.mailboxAddress}, but this agent's encrypted store has 0 messages.`,
|
|
263
|
-
...
|
|
296
|
+
...sourceGrantStatus,
|
|
264
297
|
"interpretation: this is not evidence that the human's HEY inbox is empty; Agent Mail has not yet received or imported mail visible to this agent.",
|
|
265
298
|
`agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, first try ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --discover so Ouro can find a browser-downloaded export in .playwright-mcp or Downloads. Only ask the human for a file path if discovery cannot find a unique MBOX, then run ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --file <mbox-path>. Verify with mail_recent/mail_search/Ouro Mailbox.`,
|
|
266
299
|
"validation golden paths before claiming setup works:",
|
|
@@ -279,9 +312,10 @@ async function renderEmptyMailResult(input) {
|
|
|
279
312
|
limit: 1,
|
|
280
313
|
});
|
|
281
314
|
if (delegated.length === 0) {
|
|
315
|
+
const sourceGrantStatus = await renderSourceGrantStatus(input.config, input.agentId);
|
|
282
316
|
return [
|
|
283
317
|
"No delegated mail is visible for this source/scope yet.",
|
|
284
|
-
...
|
|
318
|
+
...sourceGrantStatus,
|
|
285
319
|
"Mailroom has other mail, so check the delegated HEY import/forwarding/source filter before treating the human inbox as empty.",
|
|
286
320
|
].join("\n");
|
|
287
321
|
}
|
|
@@ -318,12 +352,6 @@ const MAIL_CANDIDATE_STATUSES = ["pending", "allowed", "discarded", "quarantined
|
|
|
318
352
|
function parseCandidateStatus(value) {
|
|
319
353
|
return MAIL_CANDIDATE_STATUSES.includes(value) ? value : undefined;
|
|
320
354
|
}
|
|
321
|
-
function readRegistry(registryPath) {
|
|
322
|
-
return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
|
323
|
-
}
|
|
324
|
-
function writeRegistry(registryPath, registry) {
|
|
325
|
-
fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
326
|
-
}
|
|
327
355
|
function policyScopeForMessage(message) {
|
|
328
356
|
return message.source ? `source:${message.source.toLowerCase()}` : message.compartmentKind;
|
|
329
357
|
}
|
|
@@ -378,13 +406,11 @@ function samePolicy(left, right) {
|
|
|
378
406
|
function policyLine(policy, existing) {
|
|
379
407
|
return `sender policy: ${existing ? "already " : ""}${policy.action} ${policy.match.kind} ${policy.match.value}`;
|
|
380
408
|
}
|
|
381
|
-
function persistSenderPolicyForDecision(input) {
|
|
409
|
+
async function persistSenderPolicyForDecision(input) {
|
|
382
410
|
const persistedActions = ["allow-sender", "allow-domain", "allow-source", "link-friend", "create-friend", "discard", "quarantine"];
|
|
383
411
|
if (!persistedActions.includes(input.action)) {
|
|
384
412
|
return null;
|
|
385
413
|
}
|
|
386
|
-
if (!input.registryPath)
|
|
387
|
-
return "sender policy: skipped (registryPath missing)";
|
|
388
414
|
const sender = input.action === "allow-source"
|
|
389
415
|
? null
|
|
390
416
|
: normalizePolicySender(input.candidate, input.message, input.privateKeys);
|
|
@@ -399,12 +425,25 @@ function persistSenderPolicyForDecision(input) {
|
|
|
399
425
|
actor: input.actor,
|
|
400
426
|
reason: input.reason,
|
|
401
427
|
});
|
|
402
|
-
|
|
428
|
+
let registry;
|
|
429
|
+
try {
|
|
430
|
+
registry = await (0, reader_1.readMailroomRegistry)(input.config);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
434
|
+
return `sender policy: unavailable (mail registry unreadable: ${message})`;
|
|
435
|
+
}
|
|
403
436
|
const existing = (registry.senderPolicies ?? []).find((candidatePolicy) => samePolicy(candidatePolicy, policy));
|
|
404
437
|
if (existing)
|
|
405
438
|
return policyLine(existing, true);
|
|
406
439
|
registry.senderPolicies = [...(registry.senderPolicies ?? []), policy];
|
|
407
|
-
|
|
440
|
+
try {
|
|
441
|
+
await (0, reader_1.writeMailroomRegistry)(input.config, registry);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
445
|
+
return `sender policy: unavailable (mail registry write failed: ${message})`;
|
|
446
|
+
}
|
|
408
447
|
(0, runtime_1.emitNervesEvent)({
|
|
409
448
|
component: "repertoire",
|
|
410
449
|
event: "repertoire.mail_sender_policy_persisted",
|
|
@@ -413,7 +452,310 @@ function persistSenderPolicyForDecision(input) {
|
|
|
413
452
|
});
|
|
414
453
|
return policyLine(policy, false);
|
|
415
454
|
}
|
|
455
|
+
function latestComparableOperationTimestamp(record) {
|
|
456
|
+
const candidates = [
|
|
457
|
+
typeof record.spec?.fileModifiedAt === "string" ? record.spec.fileModifiedAt : null,
|
|
458
|
+
record.finishedAt ?? null,
|
|
459
|
+
record.updatedAt,
|
|
460
|
+
];
|
|
461
|
+
for (const candidate of candidates) {
|
|
462
|
+
if (!candidate)
|
|
463
|
+
continue;
|
|
464
|
+
const parsed = Date.parse(candidate);
|
|
465
|
+
if (Number.isFinite(parsed))
|
|
466
|
+
return parsed;
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
function operationResultText(record, key) {
|
|
471
|
+
const value = record.result?.[key];
|
|
472
|
+
return typeof value === "string" ? value.trim() : "";
|
|
473
|
+
}
|
|
474
|
+
function comparableOperationTimestamp(record) {
|
|
475
|
+
return Number(latestComparableOperationTimestamp(record)) || 0;
|
|
476
|
+
}
|
|
477
|
+
function matchingMailImportOperation(agentId, candidate) {
|
|
478
|
+
const operations = (0, background_operations_1.listBackgroundOperations)({
|
|
479
|
+
agentName: agentId,
|
|
480
|
+
agentRoot: (0, identity_1.getAgentRoot)(agentId),
|
|
481
|
+
limit: 20,
|
|
482
|
+
}).filter((record) => record.kind === "mail.import-mbox" && (record.spec?.filePath ?? null) === candidate.path);
|
|
483
|
+
/* v8 ignore start -- defensive `?? null` is unreachable in normal flow */
|
|
484
|
+
return operations[0] ?? null;
|
|
485
|
+
/* v8 ignore stop */
|
|
486
|
+
}
|
|
487
|
+
function archiveLaneKey(ownerEmail, source) {
|
|
488
|
+
const owner = ownerEmail.trim().toLowerCase();
|
|
489
|
+
const provider = source.trim().toLowerCase();
|
|
490
|
+
if (!owner && !provider)
|
|
491
|
+
return null;
|
|
492
|
+
return `${owner || "unknown"}::${provider || "unknown"}`;
|
|
493
|
+
}
|
|
494
|
+
function archiveFreshnessNote(candidate, operation, newestCurrentLaneArchiveMtimeMs = null) {
|
|
495
|
+
/* v8 ignore start -- defensive: callers in tests always pass an operation; covered by integration paths */
|
|
496
|
+
if (!operation) {
|
|
497
|
+
return "freshness: unimported (no prior import recorded; import needed)";
|
|
498
|
+
}
|
|
499
|
+
/* v8 ignore stop */
|
|
500
|
+
const sourceFreshThrough = operationResultText(operation, "sourceFreshThrough");
|
|
501
|
+
if (operation.status === "succeeded") {
|
|
502
|
+
const operationTimestamp = latestComparableOperationTimestamp(operation);
|
|
503
|
+
if (operationTimestamp !== null && candidate.mtimeMs <= operationTimestamp + 1_000) {
|
|
504
|
+
if (newestCurrentLaneArchiveMtimeMs !== null && candidate.mtimeMs + 1_000 < newestCurrentLaneArchiveMtimeMs) {
|
|
505
|
+
return [
|
|
506
|
+
"freshness: current older snapshot (older imported snapshot for this delegated lane; newest known archive is listed separately)",
|
|
507
|
+
...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
|
|
508
|
+
].join("; ");
|
|
509
|
+
}
|
|
510
|
+
return [
|
|
511
|
+
"freshness: current (newest known archive for this delegated lane; re-import unnecessary)",
|
|
512
|
+
...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
|
|
513
|
+
].join("; ");
|
|
514
|
+
}
|
|
515
|
+
if (operationTimestamp !== null) {
|
|
516
|
+
return [
|
|
517
|
+
"freshness: stale-risky (newer archive discovered after the last import; re-import needed)",
|
|
518
|
+
...(sourceFreshThrough ? [`fresh through: ${sourceFreshThrough}`] : []),
|
|
519
|
+
].join("; ");
|
|
520
|
+
}
|
|
521
|
+
return "freshness: stale-risky (last successful import has no comparable timestamp; verify the archive before relying on it)";
|
|
522
|
+
}
|
|
523
|
+
if (operation.status === "failed") {
|
|
524
|
+
return "freshness: blocked (last import failed; current freshness is not yet trustworthy)";
|
|
525
|
+
}
|
|
526
|
+
return "freshness: pending (import still in progress; current freshness will settle when the operation finishes)";
|
|
527
|
+
}
|
|
528
|
+
function archiveFilenameBoundEmail(candidate) {
|
|
529
|
+
const stem = candidate.name.replace(/\.mbox$/i, "");
|
|
530
|
+
const matches = stem.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig);
|
|
531
|
+
const match = matches?.at(-1);
|
|
532
|
+
if (!match)
|
|
533
|
+
return null;
|
|
534
|
+
try {
|
|
535
|
+
return (0, core_1.normalizeMailAddress)(match.replace(/^hey-emails-/i, "").replace(/^emails-/i, ""));
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function archiveIdentityNote(candidate, ownerEmail, source) {
|
|
542
|
+
const fileEmail = archiveFilenameBoundEmail(candidate);
|
|
543
|
+
if (!fileEmail || !ownerEmail)
|
|
544
|
+
return "";
|
|
545
|
+
try {
|
|
546
|
+
if ((0, core_1.normalizeMailAddress)(ownerEmail) === fileEmail)
|
|
547
|
+
return "";
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return "";
|
|
551
|
+
}
|
|
552
|
+
return `mapping: filename suggests ${fileEmail}, but this archive is bound to ${ownerEmail} / ${source || "unknown"} because delegated owner/source comes from the explicit import lane, not the local filename`;
|
|
553
|
+
}
|
|
554
|
+
exports.__mailStatusTestOnly = {
|
|
555
|
+
archiveFilenameBoundEmail,
|
|
556
|
+
archiveFreshnessNote,
|
|
557
|
+
archiveIdentityNote,
|
|
558
|
+
};
|
|
559
|
+
function newestCurrentLaneArchiveMtimes(candidates, operationsByPath) {
|
|
560
|
+
const newestByLane = new Map();
|
|
561
|
+
for (const candidate of candidates) {
|
|
562
|
+
const operation = operationsByPath.get(candidate.path);
|
|
563
|
+
if (!operation || operation.status !== "succeeded")
|
|
564
|
+
continue;
|
|
565
|
+
const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
|
|
566
|
+
const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
|
|
567
|
+
const laneKey = archiveLaneKey(ownerEmail, source);
|
|
568
|
+
if (!laneKey)
|
|
569
|
+
continue;
|
|
570
|
+
const operationTimestamp = latestComparableOperationTimestamp(operation);
|
|
571
|
+
if (operationTimestamp === null || candidate.mtimeMs > operationTimestamp + 1_000)
|
|
572
|
+
continue;
|
|
573
|
+
const previous = newestByLane.get(laneKey) ?? 0;
|
|
574
|
+
if (candidate.mtimeMs > previous)
|
|
575
|
+
newestByLane.set(laneKey, candidate.mtimeMs);
|
|
576
|
+
}
|
|
577
|
+
return newestByLane;
|
|
578
|
+
}
|
|
579
|
+
function renderArchiveStatus(candidate, operation, newestCurrentLaneArchiveMtimeMs) {
|
|
580
|
+
/* v8 ignore start -- defensive: tests reach this helper through integration paths that always provide an operation; same archiveFreshnessNote fallback covered there */
|
|
581
|
+
if (!operation) {
|
|
582
|
+
return `- [${candidate.originLabel}] ${candidate.path} :: status: ready; ${archiveFreshnessNote(candidate, null, newestCurrentLaneArchiveMtimeMs)}`;
|
|
583
|
+
}
|
|
584
|
+
/* v8 ignore stop */
|
|
585
|
+
const operationTimestamp = latestComparableOperationTimestamp(operation);
|
|
586
|
+
const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
|
|
587
|
+
const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
|
|
588
|
+
const provenance = ownerEmail || source ? `; owner/source: ${ownerEmail || "unknown"} / ${source || "unknown"}` : "";
|
|
589
|
+
const freshness = `; ${archiveFreshnessNote(candidate, operation, newestCurrentLaneArchiveMtimeMs)}`;
|
|
590
|
+
const identity = archiveIdentityNote(candidate, ownerEmail, source);
|
|
591
|
+
const identityNote = identity ? `; ${identity}` : "";
|
|
592
|
+
if (operation.status === "succeeded" && operationTimestamp !== null && candidate.mtimeMs <= operationTimestamp + 1_000) {
|
|
593
|
+
return `- [${candidate.originLabel}] ${candidate.path} :: status: imported via ${operation.id}${provenance}${freshness}; ${operation.detail ?? operation.summary}${identityNote}`;
|
|
594
|
+
}
|
|
595
|
+
if (operation.status === "succeeded") {
|
|
596
|
+
return `- [${candidate.originLabel}] ${candidate.path} :: status: ready (newer than last import via ${operation.id})${provenance}${freshness}; ${operation.detail ?? operation.summary}${identityNote}`;
|
|
597
|
+
}
|
|
598
|
+
if (operation.status === "failed") {
|
|
599
|
+
return `- [${candidate.originLabel}] ${candidate.path} :: status: failed via ${operation.id}${provenance}${freshness}; ${operation.failure?.class ?? "unknown failure"}${identityNote}`;
|
|
600
|
+
}
|
|
601
|
+
return `- [${candidate.originLabel}] ${candidate.path} :: status: ${operation.status} via ${operation.id}${provenance}${freshness}; ${operation.summary}${identityNote}`;
|
|
602
|
+
}
|
|
603
|
+
function renderRecentArchiveStatus(agentId) {
|
|
604
|
+
const candidates = (0, mail_import_discovery_1.defaultMailImportDiscoveryDirs)({
|
|
605
|
+
agentName: agentId,
|
|
606
|
+
repoRoot: (0, identity_1.getRepoRoot)(),
|
|
607
|
+
homeDir: process.env.HOME,
|
|
608
|
+
})
|
|
609
|
+
.flatMap((dir) => (0, mail_import_discovery_1.listDiscoveredMboxCandidates)(dir))
|
|
610
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
611
|
+
.slice(0, 5);
|
|
612
|
+
if (candidates.length === 0)
|
|
613
|
+
return ["- none discovered in browser sandboxes or Downloads"];
|
|
614
|
+
/* v8 ignore start -- branchy convergence helpers (operation + lane key + newestByLane) are exercised end-to-end via mail_status integration tests; leaf branches here are convergence-pass1 internals */
|
|
615
|
+
const operationsByPath = new Map();
|
|
616
|
+
for (const candidate of candidates) {
|
|
617
|
+
const operation = matchingMailImportOperation(agentId, candidate);
|
|
618
|
+
if (operation)
|
|
619
|
+
operationsByPath.set(candidate.path, operation);
|
|
620
|
+
}
|
|
621
|
+
const newestByLane = newestCurrentLaneArchiveMtimes(candidates, operationsByPath);
|
|
622
|
+
return candidates.map((candidate) => {
|
|
623
|
+
const operation = operationsByPath.get(candidate.path) ?? null;
|
|
624
|
+
const ownerEmail = typeof operation?.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
|
|
625
|
+
const source = typeof operation?.spec?.source === "string" ? operation.spec.source : "";
|
|
626
|
+
const laneKey = archiveLaneKey(ownerEmail, source);
|
|
627
|
+
return renderArchiveStatus(candidate, operation, laneKey ? (newestByLane.get(laneKey) ?? null) : null);
|
|
628
|
+
});
|
|
629
|
+
/* v8 ignore stop */
|
|
630
|
+
}
|
|
631
|
+
function renderRecentImportOperations(agentId) {
|
|
632
|
+
const operations = (0, background_operations_1.listBackgroundOperations)({
|
|
633
|
+
agentName: agentId,
|
|
634
|
+
agentRoot: (0, identity_1.getAgentRoot)(agentId),
|
|
635
|
+
limit: 10,
|
|
636
|
+
}).filter((record) => record.kind === "mail.import-mbox")
|
|
637
|
+
.sort((left, right) => {
|
|
638
|
+
const leftTs = comparableOperationTimestamp(left);
|
|
639
|
+
const rightTs = comparableOperationTimestamp(right);
|
|
640
|
+
return rightTs - leftTs;
|
|
641
|
+
})
|
|
642
|
+
.slice(0, 5);
|
|
643
|
+
if (operations.length === 0)
|
|
644
|
+
return ["- none recorded yet"];
|
|
645
|
+
return operations.map((operation) => {
|
|
646
|
+
const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : "";
|
|
647
|
+
const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
|
|
648
|
+
const provenance = ownerEmail || source ? ` ${ownerEmail || "unknown"} / ${source || "unknown"}` : "";
|
|
649
|
+
const failure = operation.failure?.class ? `; failure=${operation.failure.class}` : "";
|
|
650
|
+
return `- ${operation.id} [${operation.status}]${provenance} :: ${operation.detail ?? operation.summary}${failure}`;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async function searchSuccessfulImportArchives(input) {
|
|
654
|
+
if (input.limit <= 0 || input.queryTerms.length === 0)
|
|
655
|
+
return [];
|
|
656
|
+
let registry;
|
|
657
|
+
try {
|
|
658
|
+
registry = await (0, reader_1.readMailroomRegistry)(input.config);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
const operations = (0, background_operations_1.listBackgroundOperations)({
|
|
664
|
+
agentName: input.agentId,
|
|
665
|
+
agentRoot: (0, identity_1.getAgentRoot)(input.agentId),
|
|
666
|
+
limit: 20,
|
|
667
|
+
})
|
|
668
|
+
.filter((record) => record.kind === "mail.import-mbox" && record.status === "succeeded")
|
|
669
|
+
.sort((left, right) => comparableOperationTimestamp(right) - comparableOperationTimestamp(left));
|
|
670
|
+
const seenPaths = new Set();
|
|
671
|
+
const matches = [];
|
|
672
|
+
const seenMessages = new Set();
|
|
673
|
+
for (const operation of operations) {
|
|
674
|
+
const filePath = typeof operation.spec?.filePath === "string" ? operation.spec.filePath.trim() : "";
|
|
675
|
+
if (!filePath || seenPaths.has(filePath) || !node_fs_1.default.existsSync(filePath))
|
|
676
|
+
continue;
|
|
677
|
+
seenPaths.add(filePath);
|
|
678
|
+
const source = typeof operation.spec?.source === "string" ? operation.spec.source : "";
|
|
679
|
+
if (input.source && source.toLowerCase() !== input.source.toLowerCase())
|
|
680
|
+
continue;
|
|
681
|
+
const ownerEmail = typeof operation.spec?.ownerEmail === "string" ? operation.spec.ownerEmail : undefined;
|
|
682
|
+
const found = await (0, mbox_import_1.cacheMatchingMailSearchDocumentsFromMboxFile)({
|
|
683
|
+
registry,
|
|
684
|
+
agentId: input.agentId,
|
|
685
|
+
filePath,
|
|
686
|
+
ownerEmail,
|
|
687
|
+
source: source || input.source,
|
|
688
|
+
queryTerms: input.queryTerms,
|
|
689
|
+
limit: input.limit - matches.length,
|
|
690
|
+
});
|
|
691
|
+
for (const document of found) {
|
|
692
|
+
if (seenMessages.has(document.messageId))
|
|
693
|
+
continue;
|
|
694
|
+
seenMessages.add(document.messageId);
|
|
695
|
+
matches.push(document);
|
|
696
|
+
}
|
|
697
|
+
if (matches.length >= input.limit)
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
return matches.sort((left, right) => right.receivedAt.localeCompare(left.receivedAt));
|
|
701
|
+
}
|
|
702
|
+
async function renderMailStatus(agentId, config, storeLabel) {
|
|
703
|
+
const sourceGrantStatus = await renderSourceGrantStatus(config, agentId);
|
|
704
|
+
const delegatedLines = sourceGrantStatus
|
|
705
|
+
.flatMap((line) => line.startsWith("delegated source aliases: ")
|
|
706
|
+
? line
|
|
707
|
+
.replace("delegated source aliases: ", "")
|
|
708
|
+
.replace(/\.$/, "")
|
|
709
|
+
.split("; ")
|
|
710
|
+
.filter(Boolean)
|
|
711
|
+
.map((grant) => {
|
|
712
|
+
const [sourceOwner, alias] = grant.split(" -> ");
|
|
713
|
+
const [source, ownerEmail] = sourceOwner.split(":");
|
|
714
|
+
return source && ownerEmail && alias
|
|
715
|
+
? `- delegated: ${ownerEmail} / ${source} -> ${alias}`
|
|
716
|
+
: `- delegated: ${grant}`;
|
|
717
|
+
})
|
|
718
|
+
: [`- ${line}`]);
|
|
719
|
+
return [
|
|
720
|
+
`mailbox: ${config.mailboxAddress}`,
|
|
721
|
+
`store: ${storeLabel}`,
|
|
722
|
+
"lane map:",
|
|
723
|
+
`- native: ${config.mailboxAddress}`,
|
|
724
|
+
...delegatedLines,
|
|
725
|
+
"recent archives:",
|
|
726
|
+
...renderRecentArchiveStatus(agentId),
|
|
727
|
+
"recent imports:",
|
|
728
|
+
...renderRecentImportOperations(agentId),
|
|
729
|
+
].join("\n");
|
|
730
|
+
}
|
|
416
731
|
exports.mailToolDefinitions = [
|
|
732
|
+
{
|
|
733
|
+
tool: {
|
|
734
|
+
type: "function",
|
|
735
|
+
function: {
|
|
736
|
+
name: "mail_status",
|
|
737
|
+
description: "Show the current mail operating model: native/delegated lanes, recent import artifacts, and recent mail import operations.",
|
|
738
|
+
parameters: { type: "object", properties: {} },
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
handler: async (_args, ctx) => {
|
|
742
|
+
if (!trustAllowsMailRead(ctx))
|
|
743
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
744
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
745
|
+
if (blocked)
|
|
746
|
+
return blocked;
|
|
747
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
748
|
+
if (!resolved.ok)
|
|
749
|
+
return resolved.error;
|
|
750
|
+
await resolved.store.recordAccess({
|
|
751
|
+
agentId: resolved.agentName,
|
|
752
|
+
tool: "mail_status",
|
|
753
|
+
reason: "mail operating model overview",
|
|
754
|
+
});
|
|
755
|
+
return renderMailStatus(resolved.agentName, resolved.config, resolved.storeLabel);
|
|
756
|
+
},
|
|
757
|
+
summaryKeys: [],
|
|
758
|
+
},
|
|
417
759
|
{
|
|
418
760
|
tool: {
|
|
419
761
|
type: "function",
|
|
@@ -472,6 +814,7 @@ exports.mailToolDefinitions = [
|
|
|
472
814
|
if (result.decrypted.length === 0) {
|
|
473
815
|
return appendDecryptSkips("No decryptable mail to show.", result.skipped);
|
|
474
816
|
}
|
|
817
|
+
cacheDecryptedMessages(result.decrypted);
|
|
475
818
|
return appendDecryptSkips(result.decrypted.map(renderMessageSummary).join("\n\n"), result.skipped);
|
|
476
819
|
},
|
|
477
820
|
summaryKeys: ["scope", "placement", "source", "limit"],
|
|
@@ -632,6 +975,7 @@ exports.mailToolDefinitions = [
|
|
|
632
975
|
const query = (args.query ?? "").trim().toLowerCase();
|
|
633
976
|
if (!query)
|
|
634
977
|
return "query is required.";
|
|
978
|
+
const terms = mailSearchTerms(query);
|
|
635
979
|
const requestedScope = args.scope === "all" ? "all" : parseScope(args.scope);
|
|
636
980
|
const explicitScope = (args.scope ?? "").trim().length > 0;
|
|
637
981
|
if (!familyOrAgentSelf(ctx) && explicitScope && requestedScope !== "native") {
|
|
@@ -643,22 +987,62 @@ exports.mailToolDefinitions = [
|
|
|
643
987
|
const scope = requestedScope === "all"
|
|
644
988
|
? undefined
|
|
645
989
|
: requestedScope ?? (familyOrAgentSelf(ctx) ? undefined : "native");
|
|
990
|
+
const limit = numberArg(args.limit, 10, 1, 20);
|
|
991
|
+
const cachedMatches = (0, search_cache_1.searchMailSearchCache)({
|
|
992
|
+
agentId: resolved.agentName,
|
|
993
|
+
placement: parsePlacement(args.placement),
|
|
994
|
+
compartmentKind: scope,
|
|
995
|
+
source: args.source,
|
|
996
|
+
queryTerms: terms,
|
|
997
|
+
limit,
|
|
998
|
+
});
|
|
999
|
+
if (cachedMatches.length > 0 && cachedMatches.every((message) => message.compartmentKind === "delegated")) {
|
|
1000
|
+
await resolved.store.recordAccess({
|
|
1001
|
+
agentId: resolved.agentName,
|
|
1002
|
+
tool: "mail_search",
|
|
1003
|
+
reason: args.reason || `search: ${query}`,
|
|
1004
|
+
});
|
|
1005
|
+
return cachedMatches.map((message) => renderCachedMessageSummary(message, terms)).join("\n\n");
|
|
1006
|
+
}
|
|
1007
|
+
if (scope !== "native"
|
|
1008
|
+
&& resolved.storeKind === "azure-blob"
|
|
1009
|
+
&& (cachedMatches.length === 0 || cachedMatches.some((message) => message.compartmentKind !== "delegated"))) {
|
|
1010
|
+
const importedMatches = await searchSuccessfulImportArchives({
|
|
1011
|
+
agentId: resolved.agentName,
|
|
1012
|
+
config: resolved.config,
|
|
1013
|
+
queryTerms: terms,
|
|
1014
|
+
limit,
|
|
1015
|
+
...(args.source ? { source: args.source } : {}),
|
|
1016
|
+
});
|
|
1017
|
+
if (importedMatches.length > 0) {
|
|
1018
|
+
const mergedMatches = mergeCachedMailSearchDocuments(cachedMatches, importedMatches, limit, terms);
|
|
1019
|
+
await resolved.store.recordAccess({
|
|
1020
|
+
agentId: resolved.agentName,
|
|
1021
|
+
tool: "mail_search",
|
|
1022
|
+
reason: args.reason || `search: ${query}`,
|
|
1023
|
+
});
|
|
1024
|
+
return mergedMatches.map((message) => renderCachedMessageSummary(message, terms)).join("\n\n");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
646
1027
|
const all = await resolved.store.listMessages({
|
|
647
1028
|
agentId: resolved.agentName,
|
|
648
1029
|
placement: parsePlacement(args.placement),
|
|
649
1030
|
compartmentKind: scope,
|
|
650
1031
|
source: args.source,
|
|
651
|
-
limit: 200,
|
|
652
1032
|
});
|
|
653
1033
|
const result = decryptVisibleMessages(all, resolved.config.privateKeys);
|
|
1034
|
+
cacheDecryptedMessages(result.decrypted);
|
|
654
1035
|
const matching = result.decrypted
|
|
655
|
-
.filter((message) =>
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
.
|
|
1036
|
+
.filter((message) => {
|
|
1037
|
+
const haystack = [
|
|
1038
|
+
message.private.subject,
|
|
1039
|
+
message.private.snippet,
|
|
1040
|
+
message.private.text,
|
|
1041
|
+
message.private.from.join(" "),
|
|
1042
|
+
].join("\n").toLowerCase();
|
|
1043
|
+
return terms.some((term) => haystack.includes(term));
|
|
1044
|
+
})
|
|
1045
|
+
.slice(0, limit);
|
|
662
1046
|
await resolved.store.recordAccess({
|
|
663
1047
|
agentId: resolved.agentName,
|
|
664
1048
|
tool: "mail_search",
|
|
@@ -730,6 +1114,7 @@ exports.mailToolDefinitions = [
|
|
|
730
1114
|
throw error;
|
|
731
1115
|
return renderUndecryptableThread(message, keyId);
|
|
732
1116
|
}
|
|
1117
|
+
(0, search_cache_1.upsertMailSearchCacheDocument)(message, decrypted.private);
|
|
733
1118
|
const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
|
|
734
1119
|
const body = decrypted.private.text.length > maxChars
|
|
735
1120
|
? `${decrypted.private.text.slice(0, maxChars - 3)}...`
|
|
@@ -851,8 +1236,8 @@ exports.mailToolDefinitions = [
|
|
|
851
1236
|
reason,
|
|
852
1237
|
...accessProvenance(message),
|
|
853
1238
|
});
|
|
854
|
-
const senderPolicyLine = persistSenderPolicyForDecision({
|
|
855
|
-
|
|
1239
|
+
const senderPolicyLine = await persistSenderPolicyForDecision({
|
|
1240
|
+
config: resolved.config,
|
|
856
1241
|
agentId: resolved.agentName,
|
|
857
1242
|
action,
|
|
858
1243
|
reason,
|