@ouro.bot/cli 0.1.0-alpha.454 → 0.1.0-alpha.456
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 +16 -0
- package/dist/heart/daemon/cli-exec.js +161 -66
- package/dist/heart/daemon/cli-help.js +12 -0
- package/dist/heart/daemon/cli-parse.js +13 -0
- package/dist/heart/daemon/runtime-logging.js +1 -1
- package/dist/heart/daemon/sense-manager.js +10 -2
- package/dist/heart/outlook/readers/mail.js +72 -2
- package/dist/mailroom/attention.js +154 -0
- package/dist/mailroom/blob-store.js +150 -2
- package/dist/mailroom/core.js +160 -9
- package/dist/mailroom/file-store.js +164 -2
- package/dist/mailroom/outbound.js +177 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +16 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/prompt.js +1 -1
- package/dist/outlook-ui/assets/index-BBM5EysT.js +61 -0
- package/dist/outlook-ui/assets/index-BPr5vNuM.css +1 -0
- package/dist/outlook-ui/index.html +2 -2
- package/dist/repertoire/guardrails.js +25 -0
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/repertoire/tools-mail.js +480 -4
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +224 -0
- package/package.json +1 -1
- package/dist/outlook-ui/assets/index-BXw3xmUo.js +0 -61
- package/dist/outlook-ui/assets/index-D4Wg-o8Z.css +0 -1
|
@@ -58,6 +58,9 @@ function writeJson(filePath, value) {
|
|
|
58
58
|
function compareNewestFirst(left, right) {
|
|
59
59
|
return Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
|
|
60
60
|
}
|
|
61
|
+
function compareCandidatesNewestFirst(left, right) {
|
|
62
|
+
return Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt);
|
|
63
|
+
}
|
|
61
64
|
class FileMailroomStore {
|
|
62
65
|
rootDir;
|
|
63
66
|
constructor(options) {
|
|
@@ -65,6 +68,9 @@ class FileMailroomStore {
|
|
|
65
68
|
ensureDir(this.messagesDir);
|
|
66
69
|
ensureDir(this.rawDir);
|
|
67
70
|
ensureDir(this.logsDir);
|
|
71
|
+
ensureDir(this.candidatesDir);
|
|
72
|
+
ensureDir(this.decisionsDir);
|
|
73
|
+
ensureDir(this.outboundDir);
|
|
68
74
|
(0, runtime_1.emitNervesEvent)({
|
|
69
75
|
component: "senses",
|
|
70
76
|
event: "senses.mail_file_store_init",
|
|
@@ -81,17 +87,35 @@ class FileMailroomStore {
|
|
|
81
87
|
get logsDir() {
|
|
82
88
|
return path.join(this.rootDir, "access-log");
|
|
83
89
|
}
|
|
90
|
+
get candidatesDir() {
|
|
91
|
+
return path.join(this.rootDir, "candidates");
|
|
92
|
+
}
|
|
93
|
+
get decisionsDir() {
|
|
94
|
+
return path.join(this.rootDir, "decisions");
|
|
95
|
+
}
|
|
96
|
+
get outboundDir() {
|
|
97
|
+
return path.join(this.rootDir, "outbound");
|
|
98
|
+
}
|
|
84
99
|
messagePath(id) {
|
|
85
100
|
return path.join(this.messagesDir, `${id}.json`);
|
|
86
101
|
}
|
|
102
|
+
candidatePath(id) {
|
|
103
|
+
return path.join(this.candidatesDir, `${id}.json`);
|
|
104
|
+
}
|
|
87
105
|
rawPath(objectName) {
|
|
88
106
|
return path.join(this.rootDir, objectName);
|
|
89
107
|
}
|
|
108
|
+
decisionLogPath(agentId) {
|
|
109
|
+
return path.join(this.decisionsDir, `${agentId}.jsonl`);
|
|
110
|
+
}
|
|
111
|
+
outboundPath(id) {
|
|
112
|
+
return path.join(this.outboundDir, `${id}.json`);
|
|
113
|
+
}
|
|
90
114
|
accessLogPath(agentId) {
|
|
91
115
|
return path.join(this.logsDir, `${agentId}.jsonl`);
|
|
92
116
|
}
|
|
93
117
|
async putRawMessage(input) {
|
|
94
|
-
const { message, rawPayload } = await (0, core_1.buildStoredMailMessage)(input);
|
|
118
|
+
const { message, rawPayload, candidate } = await (0, core_1.buildStoredMailMessage)(input);
|
|
95
119
|
const existing = readJson(this.messagePath(message.id));
|
|
96
120
|
if (existing) {
|
|
97
121
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -104,11 +128,14 @@ class FileMailroomStore {
|
|
|
104
128
|
}
|
|
105
129
|
writeJson(this.rawPath(message.rawObject), rawPayload);
|
|
106
130
|
writeJson(this.messagePath(message.id), message);
|
|
131
|
+
if (candidate) {
|
|
132
|
+
writeJson(this.candidatePath(candidate.id), candidate);
|
|
133
|
+
}
|
|
107
134
|
(0, runtime_1.emitNervesEvent)({
|
|
108
135
|
component: "senses",
|
|
109
136
|
event: "senses.mail_store_message_written",
|
|
110
137
|
message: "mailroom store wrote message",
|
|
111
|
-
meta: { id: message.id, agentId: message.agentId },
|
|
138
|
+
meta: { id: message.id, agentId: message.agentId, candidate: candidate !== undefined },
|
|
112
139
|
});
|
|
113
140
|
return { created: true, message };
|
|
114
141
|
}
|
|
@@ -141,6 +168,27 @@ class FileMailroomStore {
|
|
|
141
168
|
});
|
|
142
169
|
return messages;
|
|
143
170
|
}
|
|
171
|
+
async updateMessagePlacement(id, placement) {
|
|
172
|
+
const message = readJson(this.messagePath(id));
|
|
173
|
+
if (!message) {
|
|
174
|
+
(0, runtime_1.emitNervesEvent)({
|
|
175
|
+
component: "senses",
|
|
176
|
+
event: "senses.mail_store_message_placement_updated",
|
|
177
|
+
message: "mailroom store message placement update missed",
|
|
178
|
+
meta: { id, placement, found: false },
|
|
179
|
+
});
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const updated = { ...message, placement };
|
|
183
|
+
writeJson(this.messagePath(id), updated);
|
|
184
|
+
(0, runtime_1.emitNervesEvent)({
|
|
185
|
+
component: "senses",
|
|
186
|
+
event: "senses.mail_store_message_placement_updated",
|
|
187
|
+
message: "mailroom store updated message placement",
|
|
188
|
+
meta: { id, placement, found: true },
|
|
189
|
+
});
|
|
190
|
+
return updated;
|
|
191
|
+
}
|
|
144
192
|
async readRawPayload(objectName) {
|
|
145
193
|
const payload = readJson(this.rawPath(objectName));
|
|
146
194
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -151,6 +199,112 @@ class FileMailroomStore {
|
|
|
151
199
|
});
|
|
152
200
|
return payload;
|
|
153
201
|
}
|
|
202
|
+
async putScreenerCandidate(candidate) {
|
|
203
|
+
writeJson(this.candidatePath(candidate.id), candidate);
|
|
204
|
+
(0, runtime_1.emitNervesEvent)({
|
|
205
|
+
component: "senses",
|
|
206
|
+
event: "senses.mail_screener_candidate_written",
|
|
207
|
+
message: "mail screener candidate written",
|
|
208
|
+
meta: { id: candidate.id, agentId: candidate.agentId, status: candidate.status },
|
|
209
|
+
});
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
async updateScreenerCandidate(candidate) {
|
|
213
|
+
return this.putScreenerCandidate(candidate);
|
|
214
|
+
}
|
|
215
|
+
async listScreenerCandidates(filters) {
|
|
216
|
+
const candidates = fs.readdirSync(this.candidatesDir)
|
|
217
|
+
.filter((name) => name.endsWith(".json"))
|
|
218
|
+
.map((name) => readJson(path.join(this.candidatesDir, name)))
|
|
219
|
+
.filter((candidate) => candidate !== null)
|
|
220
|
+
.filter((candidate) => candidate.agentId === filters.agentId)
|
|
221
|
+
.filter((candidate) => filters.status ? candidate.status === filters.status : true)
|
|
222
|
+
.filter((candidate) => filters.placement ? candidate.placement === filters.placement : true)
|
|
223
|
+
.sort(compareCandidatesNewestFirst)
|
|
224
|
+
.slice(0, filters.limit ?? 50);
|
|
225
|
+
(0, runtime_1.emitNervesEvent)({
|
|
226
|
+
component: "senses",
|
|
227
|
+
event: "senses.mail_screener_candidates_listed",
|
|
228
|
+
message: "mail screener candidates listed",
|
|
229
|
+
meta: { agentId: filters.agentId, count: candidates.length },
|
|
230
|
+
});
|
|
231
|
+
return candidates;
|
|
232
|
+
}
|
|
233
|
+
async recordMailDecision(entry) {
|
|
234
|
+
const complete = {
|
|
235
|
+
schemaVersion: 1,
|
|
236
|
+
...entry,
|
|
237
|
+
id: entry.id ?? `decision_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
238
|
+
createdAt: entry.createdAt ?? new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
ensureDir(this.decisionsDir);
|
|
241
|
+
fs.appendFileSync(this.decisionLogPath(entry.agentId), `${JSON.stringify(complete)}\n`, "utf-8");
|
|
242
|
+
(0, runtime_1.emitNervesEvent)({
|
|
243
|
+
component: "senses",
|
|
244
|
+
event: "senses.mail_decision_recorded",
|
|
245
|
+
message: "mail decision recorded",
|
|
246
|
+
meta: { agentId: entry.agentId, messageId: entry.messageId, action: entry.action },
|
|
247
|
+
});
|
|
248
|
+
return complete;
|
|
249
|
+
}
|
|
250
|
+
async listMailDecisions(agentId) {
|
|
251
|
+
const filePath = this.decisionLogPath(agentId);
|
|
252
|
+
if (!fs.existsSync(filePath)) {
|
|
253
|
+
(0, runtime_1.emitNervesEvent)({
|
|
254
|
+
component: "senses",
|
|
255
|
+
event: "senses.mail_decisions_listed",
|
|
256
|
+
message: "mail decisions listed",
|
|
257
|
+
meta: { agentId, count: 0 },
|
|
258
|
+
});
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
const entries = fs.readFileSync(filePath, "utf-8")
|
|
262
|
+
.split(/\r?\n/)
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.map((line) => JSON.parse(line));
|
|
265
|
+
(0, runtime_1.emitNervesEvent)({
|
|
266
|
+
component: "senses",
|
|
267
|
+
event: "senses.mail_decisions_listed",
|
|
268
|
+
message: "mail decisions listed",
|
|
269
|
+
meta: { agentId, count: entries.length },
|
|
270
|
+
});
|
|
271
|
+
return entries;
|
|
272
|
+
}
|
|
273
|
+
async upsertMailOutbound(record) {
|
|
274
|
+
writeJson(this.outboundPath(record.id), record);
|
|
275
|
+
(0, runtime_1.emitNervesEvent)({
|
|
276
|
+
component: "senses",
|
|
277
|
+
event: "senses.mail_outbound_record_written",
|
|
278
|
+
message: "mail outbound record written",
|
|
279
|
+
meta: { agentId: record.agentId, id: record.id, status: record.status },
|
|
280
|
+
});
|
|
281
|
+
return record;
|
|
282
|
+
}
|
|
283
|
+
async getMailOutbound(id) {
|
|
284
|
+
const record = readJson(this.outboundPath(id));
|
|
285
|
+
(0, runtime_1.emitNervesEvent)({
|
|
286
|
+
component: "senses",
|
|
287
|
+
event: "senses.mail_outbound_record_read",
|
|
288
|
+
message: "mail outbound record read",
|
|
289
|
+
meta: { id, found: record !== null },
|
|
290
|
+
});
|
|
291
|
+
return record;
|
|
292
|
+
}
|
|
293
|
+
async listMailOutbound(agentId) {
|
|
294
|
+
const records = fs.readdirSync(this.outboundDir)
|
|
295
|
+
.filter((name) => name.endsWith(".json"))
|
|
296
|
+
.map((name) => readJson(path.join(this.outboundDir, name)))
|
|
297
|
+
.filter((record) => record !== null)
|
|
298
|
+
.filter((record) => record.agentId === agentId)
|
|
299
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
300
|
+
(0, runtime_1.emitNervesEvent)({
|
|
301
|
+
component: "senses",
|
|
302
|
+
event: "senses.mail_outbound_records_listed",
|
|
303
|
+
message: "mail outbound records listed",
|
|
304
|
+
meta: { agentId, count: records.length },
|
|
305
|
+
});
|
|
306
|
+
return records;
|
|
307
|
+
}
|
|
154
308
|
async recordAccess(entry) {
|
|
155
309
|
const complete = {
|
|
156
310
|
...entry,
|
|
@@ -194,6 +348,7 @@ class FileMailroomStore {
|
|
|
194
348
|
exports.FileMailroomStore = FileMailroomStore;
|
|
195
349
|
async function ingestRawMailToStore(input) {
|
|
196
350
|
const { resolveMailAddress } = await Promise.resolve().then(() => __importStar(require("./core")));
|
|
351
|
+
const { classifyResolvedMailPlacement } = await Promise.resolve().then(() => __importStar(require("./policy")));
|
|
197
352
|
const accepted = [];
|
|
198
353
|
const rejectedRecipients = [];
|
|
199
354
|
for (const recipient of input.envelope.rcptTo) {
|
|
@@ -202,11 +357,18 @@ async function ingestRawMailToStore(input) {
|
|
|
202
357
|
rejectedRecipients.push(recipient);
|
|
203
358
|
continue;
|
|
204
359
|
}
|
|
360
|
+
const classification = classifyResolvedMailPlacement({
|
|
361
|
+
registry: input.registry,
|
|
362
|
+
resolved,
|
|
363
|
+
sender: input.envelope.mailFrom,
|
|
364
|
+
...(input.authentication ? { authentication: input.authentication } : {}),
|
|
365
|
+
});
|
|
205
366
|
const result = await input.store.putRawMessage({
|
|
206
367
|
resolved,
|
|
207
368
|
envelope: input.envelope,
|
|
208
369
|
rawMime: input.rawMime,
|
|
209
370
|
receivedAt: input.receivedAt,
|
|
371
|
+
classification,
|
|
210
372
|
});
|
|
211
373
|
accepted.push(result.message);
|
|
212
374
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveOutboundTransport = resolveOutboundTransport;
|
|
37
|
+
exports.createMailDraft = createMailDraft;
|
|
38
|
+
exports.confirmMailDraftSend = confirmMailDraftSend;
|
|
39
|
+
exports.listMailOutboundRecords = listMailOutboundRecords;
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const runtime_1 = require("../nerves/runtime");
|
|
44
|
+
const core_1 = require("./core");
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function textField(value, key) {
|
|
49
|
+
const raw = value[key];
|
|
50
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
51
|
+
}
|
|
52
|
+
function normalizeList(values) {
|
|
53
|
+
return values
|
|
54
|
+
.map((value) => value.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map(core_1.normalizeMailAddress);
|
|
57
|
+
}
|
|
58
|
+
function draftId() {
|
|
59
|
+
return `draft_${crypto.randomBytes(12).toString("hex")}`;
|
|
60
|
+
}
|
|
61
|
+
function ensureRecipients(to) {
|
|
62
|
+
if (to.length === 0)
|
|
63
|
+
throw new Error("at least one recipient is required");
|
|
64
|
+
}
|
|
65
|
+
function resolveOutboundTransport(config) {
|
|
66
|
+
const outbound = isRecord(config) && isRecord(config.outbound) ? config.outbound : null;
|
|
67
|
+
if (!outbound) {
|
|
68
|
+
throw new Error("outbound mail transport is not configured; human-required: set mailroom.outbound before confirmed sends");
|
|
69
|
+
}
|
|
70
|
+
const transport = textField(outbound, "transport");
|
|
71
|
+
if (transport === "local-sink") {
|
|
72
|
+
const sinkPath = textField(outbound, "sinkPath");
|
|
73
|
+
if (!sinkPath)
|
|
74
|
+
throw new Error("outbound local-sink transport is missing sinkPath");
|
|
75
|
+
return { kind: "local-sink", sinkPath };
|
|
76
|
+
}
|
|
77
|
+
if (transport === "azure-communication-services") {
|
|
78
|
+
const endpoint = textField(outbound, "endpoint");
|
|
79
|
+
if (!endpoint)
|
|
80
|
+
throw new Error("outbound Azure Communication Services transport is missing endpoint");
|
|
81
|
+
const senderAddress = textField(outbound, "senderAddress");
|
|
82
|
+
return {
|
|
83
|
+
kind: "azure-communication-services",
|
|
84
|
+
endpoint,
|
|
85
|
+
...(senderAddress ? { senderAddress } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
throw new Error("outbound mail transport is not configured; human-required: choose local-sink or azure-communication-services");
|
|
89
|
+
}
|
|
90
|
+
async function createMailDraft(input) {
|
|
91
|
+
const now = (input.now ?? (() => new Date()))().toISOString();
|
|
92
|
+
const to = normalizeList(input.to);
|
|
93
|
+
ensureRecipients(to);
|
|
94
|
+
const record = {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
id: draftId(),
|
|
97
|
+
agentId: input.agentId,
|
|
98
|
+
status: "draft",
|
|
99
|
+
from: (0, core_1.normalizeMailAddress)(input.from),
|
|
100
|
+
to,
|
|
101
|
+
cc: normalizeList(input.cc ?? []),
|
|
102
|
+
bcc: normalizeList(input.bcc ?? []),
|
|
103
|
+
subject: input.subject.trim(),
|
|
104
|
+
text: input.text,
|
|
105
|
+
actor: input.actor,
|
|
106
|
+
reason: input.reason,
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now,
|
|
109
|
+
};
|
|
110
|
+
await input.store.upsertMailOutbound(record);
|
|
111
|
+
(0, runtime_1.emitNervesEvent)({
|
|
112
|
+
component: "senses",
|
|
113
|
+
event: "senses.mail_draft_created",
|
|
114
|
+
message: "mail draft created",
|
|
115
|
+
meta: { agentId: record.agentId, id: record.id, toCount: record.to.length },
|
|
116
|
+
});
|
|
117
|
+
return record;
|
|
118
|
+
}
|
|
119
|
+
function appendLocalSink(transport, record, sentAt) {
|
|
120
|
+
fs.mkdirSync(path.dirname(transport.sinkPath), { recursive: true });
|
|
121
|
+
const transportMessageId = `local_${crypto.randomBytes(10).toString("hex")}`;
|
|
122
|
+
fs.appendFileSync(transport.sinkPath, `${JSON.stringify({
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
transportMessageId,
|
|
125
|
+
draftId: record.id,
|
|
126
|
+
agentId: record.agentId,
|
|
127
|
+
from: record.from,
|
|
128
|
+
to: record.to,
|
|
129
|
+
cc: record.cc,
|
|
130
|
+
bcc: record.bcc,
|
|
131
|
+
subject: record.subject,
|
|
132
|
+
text: record.text,
|
|
133
|
+
sentAt,
|
|
134
|
+
})}\n`, "utf-8");
|
|
135
|
+
return transportMessageId;
|
|
136
|
+
}
|
|
137
|
+
function transportSend(transport, record, sentAt) {
|
|
138
|
+
if (transport.kind === "local-sink")
|
|
139
|
+
return appendLocalSink(transport, record, sentAt);
|
|
140
|
+
throw new Error("Azure Communication Services outbound send is configured but not enabled on this machine; human-required setup is still needed");
|
|
141
|
+
}
|
|
142
|
+
async function confirmMailDraftSend(input) {
|
|
143
|
+
if (input.autonomous) {
|
|
144
|
+
throw new Error("Autonomous mail sending is disabled; create a draft and require explicit confirmation instead");
|
|
145
|
+
}
|
|
146
|
+
if (input.confirmation !== "CONFIRM_SEND") {
|
|
147
|
+
throw new Error("mail_send requires confirmation=CONFIRM_SEND before any outbound mail leaves the agent");
|
|
148
|
+
}
|
|
149
|
+
const draft = await input.store.getMailOutbound(input.draftId);
|
|
150
|
+
if (!draft || draft.agentId !== input.agentId)
|
|
151
|
+
throw new Error(`No draft found for ${input.draftId}`);
|
|
152
|
+
if (draft.status !== "draft")
|
|
153
|
+
throw new Error(`Draft ${input.draftId} is already ${draft.status}`);
|
|
154
|
+
const sentAt = (input.now ?? (() => new Date()))().toISOString();
|
|
155
|
+
const transportMessageId = transportSend(input.transport, draft, sentAt);
|
|
156
|
+
const sent = {
|
|
157
|
+
...draft,
|
|
158
|
+
status: "sent",
|
|
159
|
+
actor: input.actor,
|
|
160
|
+
reason: input.reason,
|
|
161
|
+
updatedAt: sentAt,
|
|
162
|
+
sentAt,
|
|
163
|
+
transport: input.transport.kind,
|
|
164
|
+
transportMessageId,
|
|
165
|
+
};
|
|
166
|
+
await input.store.upsertMailOutbound(sent);
|
|
167
|
+
(0, runtime_1.emitNervesEvent)({
|
|
168
|
+
component: "senses",
|
|
169
|
+
event: "senses.mail_draft_sent",
|
|
170
|
+
message: "mail draft sent",
|
|
171
|
+
meta: { agentId: sent.agentId, id: sent.id, transport: sent.transport },
|
|
172
|
+
});
|
|
173
|
+
return sent;
|
|
174
|
+
}
|
|
175
|
+
function listMailOutboundRecords(store, agentId) {
|
|
176
|
+
return store.listMailOutbound(agentId);
|
|
177
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.buildSenderPolicy = buildSenderPolicy;
|
|
37
|
+
exports.classifyResolvedMailPlacement = classifyResolvedMailPlacement;
|
|
38
|
+
exports.classifyMailPlacement = classifyMailPlacement;
|
|
39
|
+
exports.listPendingScreenerCandidates = listPendingScreenerCandidates;
|
|
40
|
+
exports.applyMailDecision = applyMailDecision;
|
|
41
|
+
const crypto = __importStar(require("node:crypto"));
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const core_1 = require("./core");
|
|
44
|
+
function stableJson(value) {
|
|
45
|
+
/* v8 ignore next -- current sender-policy IDs are built from object/scalar fields; array support is defensive. @preserve */
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
48
|
+
if (value && typeof value === "object") {
|
|
49
|
+
const record = value;
|
|
50
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
function policyId(input) {
|
|
55
|
+
return `policy_${crypto.createHash("sha256").update(stableJson({
|
|
56
|
+
agentId: input.agentId.toLowerCase(),
|
|
57
|
+
scope: input.scope,
|
|
58
|
+
match: normalizeMatch(input.match),
|
|
59
|
+
action: input.action,
|
|
60
|
+
reason: input.reason,
|
|
61
|
+
})).digest("hex").slice(0, 16)}`;
|
|
62
|
+
}
|
|
63
|
+
function normalizeSender(sender) {
|
|
64
|
+
try {
|
|
65
|
+
return (0, core_1.normalizeMailAddress)(sender);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function senderDomain(sender) {
|
|
72
|
+
if (!sender)
|
|
73
|
+
return null;
|
|
74
|
+
/* v8 ignore next -- normalizeMailAddress guarantees a domain for non-null senders. @preserve */
|
|
75
|
+
return sender.split("@")[1]?.toLowerCase() ?? null;
|
|
76
|
+
}
|
|
77
|
+
function normalizeMatch(match) {
|
|
78
|
+
if (match.kind === "email")
|
|
79
|
+
return { kind: "email", value: (0, core_1.normalizeMailAddress)(match.value) };
|
|
80
|
+
return { kind: match.kind, value: match.value.trim().toLowerCase() };
|
|
81
|
+
}
|
|
82
|
+
function authenticationFailed(authentication) {
|
|
83
|
+
if (!authentication)
|
|
84
|
+
return false;
|
|
85
|
+
return authentication.dmarc === "fail" || (authentication.spf === "fail" &&
|
|
86
|
+
authentication.dkim === "fail");
|
|
87
|
+
}
|
|
88
|
+
function scopeMatches(policy, resolved) {
|
|
89
|
+
if (policy.scope === "all")
|
|
90
|
+
return true;
|
|
91
|
+
if (policy.scope === resolved.compartmentKind)
|
|
92
|
+
return true;
|
|
93
|
+
if (policy.scope.startsWith("source:"))
|
|
94
|
+
return resolved.source?.toLowerCase() === policy.scope.slice("source:".length);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function policyMatches(policy, resolved, sender) {
|
|
98
|
+
if (policy.agentId !== resolved.agentId || !scopeMatches(policy, resolved))
|
|
99
|
+
return false;
|
|
100
|
+
const match = normalizeMatch(policy.match);
|
|
101
|
+
if (match.kind === "email")
|
|
102
|
+
return sender === match.value;
|
|
103
|
+
if (match.kind === "domain")
|
|
104
|
+
return senderDomain(sender) === match.value;
|
|
105
|
+
if (match.kind === "source")
|
|
106
|
+
return resolved.source?.toLowerCase() === match.value;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function classificationForPolicy(policy) {
|
|
110
|
+
const placement = policy.action === "allow"
|
|
111
|
+
? "imbox"
|
|
112
|
+
: policy.action === "discard"
|
|
113
|
+
? "discarded"
|
|
114
|
+
: "quarantine";
|
|
115
|
+
return {
|
|
116
|
+
placement,
|
|
117
|
+
candidate: false,
|
|
118
|
+
trustReason: `sender policy ${policy.action} ${policy.match.kind} ${normalizeMatch(policy.match).value}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function buildSenderPolicy(input) {
|
|
122
|
+
const policy = {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
policyId: policyId(input),
|
|
125
|
+
agentId: input.agentId.toLowerCase(),
|
|
126
|
+
scope: input.scope,
|
|
127
|
+
match: normalizeMatch(input.match),
|
|
128
|
+
action: input.action,
|
|
129
|
+
actor: input.actor,
|
|
130
|
+
reason: input.reason,
|
|
131
|
+
createdAt: (input.now ?? new Date()).toISOString(),
|
|
132
|
+
};
|
|
133
|
+
(0, runtime_1.emitNervesEvent)({
|
|
134
|
+
component: "senses",
|
|
135
|
+
event: "senses.mail_sender_policy_built",
|
|
136
|
+
message: "mail sender policy built",
|
|
137
|
+
meta: { agentId: policy.agentId, action: policy.action, scope: policy.scope, matchKind: policy.match.kind },
|
|
138
|
+
});
|
|
139
|
+
return policy;
|
|
140
|
+
}
|
|
141
|
+
function classifyResolvedMailPlacement(input) {
|
|
142
|
+
if (authenticationFailed(input.authentication)) {
|
|
143
|
+
const classification = {
|
|
144
|
+
placement: "quarantine",
|
|
145
|
+
candidate: false,
|
|
146
|
+
trustReason: "mail authentication failed",
|
|
147
|
+
authentication: input.authentication,
|
|
148
|
+
};
|
|
149
|
+
(0, runtime_1.emitNervesEvent)({
|
|
150
|
+
component: "senses",
|
|
151
|
+
event: "senses.mail_classified",
|
|
152
|
+
message: "mail classified by authentication failure",
|
|
153
|
+
meta: { agentId: input.resolved.agentId, placement: classification.placement },
|
|
154
|
+
});
|
|
155
|
+
return classification;
|
|
156
|
+
}
|
|
157
|
+
const sender = normalizeSender(input.sender);
|
|
158
|
+
const policy = input.registry.senderPolicies?.find((entry) => policyMatches(entry, input.resolved, sender));
|
|
159
|
+
if (policy) {
|
|
160
|
+
const classification = classificationForPolicy(policy);
|
|
161
|
+
(0, runtime_1.emitNervesEvent)({
|
|
162
|
+
component: "senses",
|
|
163
|
+
event: "senses.mail_classified",
|
|
164
|
+
message: "mail classified by sender policy",
|
|
165
|
+
meta: { agentId: input.resolved.agentId, placement: classification.placement, policyId: policy.policyId },
|
|
166
|
+
});
|
|
167
|
+
return classification;
|
|
168
|
+
}
|
|
169
|
+
const placement = input.resolved.defaultPlacement;
|
|
170
|
+
const classification = {
|
|
171
|
+
placement,
|
|
172
|
+
candidate: placement === "screener",
|
|
173
|
+
trustReason: input.resolved.compartmentKind === "delegated"
|
|
174
|
+
? `delegated source grant ${input.resolved.source ?? input.resolved.compartmentId}`
|
|
175
|
+
: placement === "imbox"
|
|
176
|
+
? "screened-in native agent mailbox"
|
|
177
|
+
: "native agent mailbox default screener",
|
|
178
|
+
...(input.authentication ? { authentication: input.authentication } : {}),
|
|
179
|
+
};
|
|
180
|
+
(0, runtime_1.emitNervesEvent)({
|
|
181
|
+
component: "senses",
|
|
182
|
+
event: "senses.mail_classified",
|
|
183
|
+
message: "mail classified by default placement",
|
|
184
|
+
meta: { agentId: input.resolved.agentId, placement: classification.placement, candidate: classification.candidate },
|
|
185
|
+
});
|
|
186
|
+
return classification;
|
|
187
|
+
}
|
|
188
|
+
function classifyMailPlacement(input) {
|
|
189
|
+
const resolved = (0, core_1.resolveMailAddress)(input.registry, input.recipient);
|
|
190
|
+
if (!resolved)
|
|
191
|
+
throw new Error(`Cannot classify unknown mail recipient ${input.recipient}`);
|
|
192
|
+
return classifyResolvedMailPlacement({
|
|
193
|
+
registry: input.registry,
|
|
194
|
+
resolved,
|
|
195
|
+
sender: input.sender,
|
|
196
|
+
...(input.authentication ? { authentication: input.authentication } : {}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function decisionPlacement(action) {
|
|
200
|
+
if (action === "discard")
|
|
201
|
+
return "discarded";
|
|
202
|
+
if (action === "quarantine")
|
|
203
|
+
return "quarantine";
|
|
204
|
+
return "imbox";
|
|
205
|
+
}
|
|
206
|
+
function candidateStatus(action) {
|
|
207
|
+
if (action === "discard")
|
|
208
|
+
return "discarded";
|
|
209
|
+
if (action === "quarantine")
|
|
210
|
+
return "quarantined";
|
|
211
|
+
if (action === "restore")
|
|
212
|
+
return "restored";
|
|
213
|
+
return "allowed";
|
|
214
|
+
}
|
|
215
|
+
async function listPendingScreenerCandidates(store, agentId) {
|
|
216
|
+
const candidates = await store.listScreenerCandidates({ agentId, status: "pending" });
|
|
217
|
+
(0, runtime_1.emitNervesEvent)({
|
|
218
|
+
component: "senses",
|
|
219
|
+
event: "senses.mail_pending_screener_candidates_listed",
|
|
220
|
+
message: "pending mail screener candidates listed",
|
|
221
|
+
meta: { agentId, count: candidates.length },
|
|
222
|
+
});
|
|
223
|
+
return candidates;
|
|
224
|
+
}
|
|
225
|
+
async function applyMailDecision(input) {
|
|
226
|
+
const message = await input.store.getMessage(input.messageId);
|
|
227
|
+
if (!message || message.agentId !== input.agentId) {
|
|
228
|
+
throw new Error(`No mail message ${input.messageId} for ${input.agentId}`);
|
|
229
|
+
}
|
|
230
|
+
const candidate = (await input.store.listScreenerCandidates({ agentId: input.agentId }))
|
|
231
|
+
.find((entry) => entry.messageId === input.messageId);
|
|
232
|
+
const nextPlacement = decisionPlacement(input.action);
|
|
233
|
+
const decision = await input.store.recordMailDecision({
|
|
234
|
+
agentId: input.agentId,
|
|
235
|
+
messageId: input.messageId,
|
|
236
|
+
...(candidate ? { candidateId: candidate.id } : {}),
|
|
237
|
+
action: input.action,
|
|
238
|
+
actor: input.actor,
|
|
239
|
+
reason: input.reason,
|
|
240
|
+
previousPlacement: message.placement,
|
|
241
|
+
nextPlacement,
|
|
242
|
+
...(candidate?.senderEmail ? { senderEmail: candidate.senderEmail } : {}),
|
|
243
|
+
...(input.friendId ? { friendId: input.friendId } : {}),
|
|
244
|
+
...(input.now ? { createdAt: input.now.toISOString() } : {}),
|
|
245
|
+
});
|
|
246
|
+
await input.store.updateMessagePlacement(input.messageId, nextPlacement);
|
|
247
|
+
if (candidate) {
|
|
248
|
+
await input.store.updateScreenerCandidate({
|
|
249
|
+
...candidate,
|
|
250
|
+
placement: nextPlacement,
|
|
251
|
+
status: candidateStatus(input.action),
|
|
252
|
+
lastSeenAt: decision.createdAt,
|
|
253
|
+
resolvedByDecisionId: decision.id,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
(0, runtime_1.emitNervesEvent)({
|
|
257
|
+
component: "senses",
|
|
258
|
+
event: "senses.mail_decision_applied",
|
|
259
|
+
message: "mail decision applied",
|
|
260
|
+
meta: { agentId: input.agentId, messageId: input.messageId, action: input.action, nextPlacement },
|
|
261
|
+
});
|
|
262
|
+
return decision;
|
|
263
|
+
}
|