@ouro.bot/cli 0.1.0-alpha.454 → 0.1.0-alpha.455
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 +8 -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-BPr5vNuM.css +1 -0
- package/dist/outlook-ui/assets/index-BSNvyKGt.js +61 -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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="color-scheme" content="dark" />
|
|
7
7
|
<title>Ouro Outlook</title>
|
|
8
8
|
<meta name="description" content="The daemon-hosted shared orientation surface for agents alive on this machine." />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BSNvyKGt.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BPr5vNuM.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="app"></div>
|
|
@@ -301,6 +301,28 @@ const CREDENTIAL_TRUSTED_TOOLS = new Set(["credential_get", "credential_list"]);
|
|
|
301
301
|
// advisory and geocode are public APIs but gated for consistency)
|
|
302
302
|
// Flight search is also friend+ (read-only, no payment)
|
|
303
303
|
const TRAVEL_TRUSTED_TOOLS = new Set(["weather_lookup", "travel_advisory", "geocode_search", "flight_search"]);
|
|
304
|
+
const MAIL_FAMILY_TOOLS = new Set(["mail_screener", "mail_decide", "mail_access_log", "mail_send"]);
|
|
305
|
+
const MAIL_DELEGATED_READ_TOOLS = new Set(["mail_recent", "mail_search"]);
|
|
306
|
+
function mailTrustGuardrail(toolName, args, context) {
|
|
307
|
+
if (MAIL_FAMILY_TOOLS.has(toolName)) {
|
|
308
|
+
if (context.trustLevel === undefined || context.trustLevel === "family")
|
|
309
|
+
return allow;
|
|
310
|
+
if (toolName === "mail_send")
|
|
311
|
+
return deny("outbound mail sends require family trust.");
|
|
312
|
+
return deny(toolName === "mail_decide"
|
|
313
|
+
? "mail screener decisions require family trust."
|
|
314
|
+
: "delegated human mail requires family trust.");
|
|
315
|
+
}
|
|
316
|
+
if (MAIL_DELEGATED_READ_TOOLS.has(toolName)) {
|
|
317
|
+
const scope = (args.scope ?? "").trim().toLowerCase();
|
|
318
|
+
if (scope === "delegated" || scope === "all") {
|
|
319
|
+
if (context.trustLevel === undefined || context.trustLevel === "family")
|
|
320
|
+
return allow;
|
|
321
|
+
return deny("delegated human mail requires family trust.");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return allow;
|
|
325
|
+
}
|
|
304
326
|
function checkCredentialTrustGuardrails(toolName, context) {
|
|
305
327
|
if (CREDENTIAL_FAMILY_TOOLS.has(toolName)) {
|
|
306
328
|
if (context.trustLevel === "family")
|
|
@@ -329,6 +351,9 @@ function checkFirstClassMcpTrust(context) {
|
|
|
329
351
|
return allow;
|
|
330
352
|
}
|
|
331
353
|
function checkTrustLevelGuardrails(toolName, args, context) {
|
|
354
|
+
const mailResult = mailTrustGuardrail(toolName, args, context);
|
|
355
|
+
if (!mailResult.allowed)
|
|
356
|
+
return mailResult;
|
|
332
357
|
// Credential tools have their own trust rules that apply at all levels
|
|
333
358
|
const credentialResult = checkCredentialTrustGuardrails(toolName, context);
|
|
334
359
|
if (!credentialResult.allowed)
|
|
@@ -16,6 +16,7 @@ const tools_user_profile_1 = require("./tools-user-profile");
|
|
|
16
16
|
const tools_stripe_1 = require("./tools-stripe");
|
|
17
17
|
const tools_flight_1 = require("./tools-flight");
|
|
18
18
|
const tools_attachments_1 = require("./tools-attachments");
|
|
19
|
+
const tools_mail_1 = require("./tools-mail");
|
|
19
20
|
// Re-export flow tools for consumers that import them from tools-base
|
|
20
21
|
var tools_flow_1 = require("./tools-flow");
|
|
21
22
|
Object.defineProperty(exports, "ponderTool", { enumerable: true, get: function () { return tools_flow_1.ponderTool; } });
|
|
@@ -46,6 +47,7 @@ exports.baseToolDefinitions = [
|
|
|
46
47
|
...tools_stripe_1.stripeToolDefinitions,
|
|
47
48
|
...tools_flight_1.flightToolDefinitions,
|
|
48
49
|
...tools_attachments_1.attachmentToolDefinitions,
|
|
50
|
+
...tools_mail_1.mailToolDefinitions,
|
|
49
51
|
];
|
|
50
52
|
// Convenience array of just the tool schemas (no handler/integration metadata).
|
|
51
53
|
// Used by consumers that need the OpenAI function-tool format.
|
|
@@ -1,9 +1,46 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.mailToolDefinitions = void 0;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
4
38
|
const types_1 = require("../mind/friends/types");
|
|
5
39
|
const file_store_1 = require("../mailroom/file-store");
|
|
6
40
|
const reader_1 = require("../mailroom/reader");
|
|
41
|
+
const outbound_1 = require("../mailroom/outbound");
|
|
42
|
+
const policy_1 = require("../mailroom/policy");
|
|
43
|
+
const core_1 = require("../mailroom/core");
|
|
7
44
|
const runtime_1 = require("../nerves/runtime");
|
|
8
45
|
function trustAllowsMailRead(ctx) {
|
|
9
46
|
const trustLevel = ctx?.context?.friend?.trustLevel;
|
|
@@ -16,12 +53,44 @@ function trustAllowsMailRead(ctx) {
|
|
|
16
53
|
});
|
|
17
54
|
return allowed;
|
|
18
55
|
}
|
|
56
|
+
function familyOrAgentSelf(ctx) {
|
|
57
|
+
const trustLevel = ctx?.context?.friend?.trustLevel;
|
|
58
|
+
return trustLevel === undefined || trustLevel === "family";
|
|
59
|
+
}
|
|
60
|
+
function delegatedHumanMailBlocked(ctx) {
|
|
61
|
+
if (familyOrAgentSelf(ctx))
|
|
62
|
+
return null;
|
|
63
|
+
return "delegated human mail requires family trust.";
|
|
64
|
+
}
|
|
65
|
+
function screenerDecisionBlocked(ctx) {
|
|
66
|
+
if (familyOrAgentSelf(ctx))
|
|
67
|
+
return null;
|
|
68
|
+
return "mail screener decisions require family trust.";
|
|
69
|
+
}
|
|
70
|
+
function outboundSendBlocked(ctx) {
|
|
71
|
+
if (familyOrAgentSelf(ctx))
|
|
72
|
+
return null;
|
|
73
|
+
return "outbound mail sends require family trust.";
|
|
74
|
+
}
|
|
19
75
|
function numberArg(value, fallback, min, max) {
|
|
20
76
|
const parsed = value ? Number.parseInt(value, 10) : fallback;
|
|
21
77
|
if (!Number.isFinite(parsed))
|
|
22
78
|
return fallback;
|
|
23
79
|
return Math.min(max, Math.max(min, parsed));
|
|
24
80
|
}
|
|
81
|
+
const MAIL_PLACEMENTS = ["imbox", "screener", "discarded", "quarantine", "draft", "sent"];
|
|
82
|
+
function parsePlacement(value) {
|
|
83
|
+
return MAIL_PLACEMENTS.includes(value) ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
function parseScope(value) {
|
|
86
|
+
return value === "native" || value === "delegated" ? value : undefined;
|
|
87
|
+
}
|
|
88
|
+
function parseMailList(value) {
|
|
89
|
+
return (value ?? "")
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((entry) => entry.trim())
|
|
92
|
+
.filter(Boolean);
|
|
93
|
+
}
|
|
25
94
|
function renderMessageSummary(message) {
|
|
26
95
|
const scope = message.compartmentKind === "delegated"
|
|
27
96
|
? `delegated:${message.ownerEmail ?? "unknown"}:${message.source ?? "source"}`
|
|
@@ -36,6 +105,18 @@ function renderMessageSummary(message) {
|
|
|
36
105
|
` warning: ${message.private.untrustedContentWarning}`,
|
|
37
106
|
].join("\n");
|
|
38
107
|
}
|
|
108
|
+
function renderScreenerCandidate(candidate) {
|
|
109
|
+
const delegated = candidate.ownerEmail || candidate.source
|
|
110
|
+
? ` delegated:${candidate.ownerEmail ?? "unknown"}:${candidate.source ?? "source"}`
|
|
111
|
+
: "";
|
|
112
|
+
return [
|
|
113
|
+
`- ${candidate.id} -> ${candidate.messageId} [${candidate.status}; ${candidate.placement}${delegated}]`,
|
|
114
|
+
` sender: ${candidate.senderDisplay || candidate.senderEmail} <${candidate.senderEmail}>`,
|
|
115
|
+
` recipient: ${candidate.recipient}`,
|
|
116
|
+
` last seen: ${candidate.lastSeenAt}; messages: ${candidate.messageCount}`,
|
|
117
|
+
` reason: ${candidate.trustReason}`,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
39
120
|
function renderAccessLog(entries) {
|
|
40
121
|
if (entries.length === 0)
|
|
41
122
|
return "No mail access records yet.";
|
|
@@ -48,6 +129,124 @@ function renderAccessLog(entries) {
|
|
|
48
129
|
})
|
|
49
130
|
.join("\n");
|
|
50
131
|
}
|
|
132
|
+
function actorFromContext(ctx, agentId) {
|
|
133
|
+
const friend = ctx?.context?.friend;
|
|
134
|
+
if (friend) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "human",
|
|
137
|
+
friendId: friend.id,
|
|
138
|
+
trustLevel: friend.trustLevel,
|
|
139
|
+
channel: ctx?.context?.channel.channel,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { kind: "agent", agentId };
|
|
143
|
+
}
|
|
144
|
+
const MAIL_DECISION_ACTIONS = [
|
|
145
|
+
"link-friend",
|
|
146
|
+
"create-friend",
|
|
147
|
+
"allow-sender",
|
|
148
|
+
"allow-source",
|
|
149
|
+
"allow-domain",
|
|
150
|
+
"allow-thread",
|
|
151
|
+
"discard",
|
|
152
|
+
"quarantine",
|
|
153
|
+
"restore",
|
|
154
|
+
];
|
|
155
|
+
function parseDecisionAction(value) {
|
|
156
|
+
return MAIL_DECISION_ACTIONS.includes(value) ? value : null;
|
|
157
|
+
}
|
|
158
|
+
const MAIL_CANDIDATE_STATUSES = ["pending", "allowed", "discarded", "quarantined", "restored"];
|
|
159
|
+
function parseCandidateStatus(value) {
|
|
160
|
+
return MAIL_CANDIDATE_STATUSES.includes(value) ? value : undefined;
|
|
161
|
+
}
|
|
162
|
+
function readRegistry(registryPath) {
|
|
163
|
+
return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
|
164
|
+
}
|
|
165
|
+
function writeRegistry(registryPath, registry) {
|
|
166
|
+
fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
function policyScopeForMessage(message) {
|
|
169
|
+
return message.source ? `source:${message.source.toLowerCase()}` : message.compartmentKind;
|
|
170
|
+
}
|
|
171
|
+
function normalizePolicySender(candidate, message, privateKeys) {
|
|
172
|
+
const candidates = [
|
|
173
|
+
candidate?.senderEmail,
|
|
174
|
+
...(0, file_store_1.decryptMessages)([message], privateKeys)[0].private.from,
|
|
175
|
+
message.envelope.mailFrom,
|
|
176
|
+
].filter((value) => typeof value === "string" && value.trim().length > 0 && value !== "(unknown)");
|
|
177
|
+
for (const candidateValue of candidates) {
|
|
178
|
+
try {
|
|
179
|
+
return (0, core_1.normalizeMailAddress)(candidateValue);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Try the next source of sender truth.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/* v8 ignore next -- exhaustive fallback: current persisted-policy actions are handled above. @preserve */
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function policyMatchForDecision(input) {
|
|
189
|
+
if (input.action === "allow-source") {
|
|
190
|
+
if (!input.message.source)
|
|
191
|
+
return null;
|
|
192
|
+
return {
|
|
193
|
+
match: { kind: "source", value: input.message.source.toLowerCase() },
|
|
194
|
+
scope: `source:${input.message.source.toLowerCase()}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (!input.sender)
|
|
198
|
+
return null;
|
|
199
|
+
if (input.action === "allow-domain") {
|
|
200
|
+
const domain = input.sender.slice(input.sender.indexOf("@") + 1);
|
|
201
|
+
return { match: { kind: "domain", value: domain }, scope: policyScopeForMessage(input.message) };
|
|
202
|
+
}
|
|
203
|
+
return { match: { kind: "email", value: input.sender }, scope: policyScopeForMessage(input.message) };
|
|
204
|
+
}
|
|
205
|
+
function samePolicy(left, right) {
|
|
206
|
+
return left.agentId === right.agentId &&
|
|
207
|
+
left.action === right.action &&
|
|
208
|
+
left.scope === right.scope &&
|
|
209
|
+
left.match.kind === right.match.kind &&
|
|
210
|
+
left.match.value === right.match.value;
|
|
211
|
+
}
|
|
212
|
+
function policyLine(policy, existing) {
|
|
213
|
+
return `sender policy: ${existing ? "already " : ""}${policy.action} ${policy.match.kind} ${policy.match.value}`;
|
|
214
|
+
}
|
|
215
|
+
function persistSenderPolicyForDecision(input) {
|
|
216
|
+
const persistedActions = ["allow-sender", "allow-domain", "allow-source", "link-friend", "create-friend", "discard", "quarantine"];
|
|
217
|
+
if (!persistedActions.includes(input.action)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (!input.registryPath)
|
|
221
|
+
return "sender policy: skipped (registryPath missing)";
|
|
222
|
+
const sender = input.action === "allow-source"
|
|
223
|
+
? null
|
|
224
|
+
: normalizePolicySender(input.candidate, input.message, input.privateKeys);
|
|
225
|
+
const match = policyMatchForDecision({ action: input.action, sender, message: input.message });
|
|
226
|
+
if (!match)
|
|
227
|
+
return "sender policy: skipped (sender/source unavailable)";
|
|
228
|
+
const policy = (0, policy_1.buildSenderPolicy)({
|
|
229
|
+
agentId: input.agentId,
|
|
230
|
+
scope: match.scope,
|
|
231
|
+
match: match.match,
|
|
232
|
+
action: input.action === "discard" || input.action === "quarantine" ? input.action : "allow",
|
|
233
|
+
actor: input.actor,
|
|
234
|
+
reason: input.reason,
|
|
235
|
+
});
|
|
236
|
+
const registry = readRegistry(input.registryPath);
|
|
237
|
+
const existing = (registry.senderPolicies ?? []).find((candidatePolicy) => samePolicy(candidatePolicy, policy));
|
|
238
|
+
if (existing)
|
|
239
|
+
return policyLine(existing, true);
|
|
240
|
+
registry.senderPolicies = [...(registry.senderPolicies ?? []), policy];
|
|
241
|
+
writeRegistry(input.registryPath, registry);
|
|
242
|
+
(0, runtime_1.emitNervesEvent)({
|
|
243
|
+
component: "repertoire",
|
|
244
|
+
event: "repertoire.mail_sender_policy_persisted",
|
|
245
|
+
message: "mail sender policy persisted from screener decision",
|
|
246
|
+
meta: { agentId: input.agentId, action: policy.action, scope: policy.scope, matchKind: policy.match.kind },
|
|
247
|
+
});
|
|
248
|
+
return policyLine(policy, false);
|
|
249
|
+
}
|
|
51
250
|
exports.mailToolDefinitions = [
|
|
52
251
|
{
|
|
53
252
|
tool: {
|
|
@@ -59,7 +258,7 @@ exports.mailToolDefinitions = [
|
|
|
59
258
|
type: "object",
|
|
60
259
|
properties: {
|
|
61
260
|
limit: { type: "string", description: "Maximum messages to return, 1-20. Defaults to 10." },
|
|
62
|
-
placement: { type: "string", enum: ["imbox", "screener"], description: "Optional
|
|
261
|
+
placement: { type: "string", enum: ["imbox", "screener", "discarded", "quarantine", "draft", "sent"], description: "Optional mailbox placement filter." },
|
|
63
262
|
scope: { type: "string", enum: ["native", "delegated", "all"], description: "Optional mailbox scope. Defaults to all visible mail." },
|
|
64
263
|
source: { type: "string", description: "Optional delegated source filter, e.g. hey." },
|
|
65
264
|
reason: { type: "string", description: "Why you are looking at this mail. Logged for audit." },
|
|
@@ -70,13 +269,21 @@ exports.mailToolDefinitions = [
|
|
|
70
269
|
handler: async (args, ctx) => {
|
|
71
270
|
if (!trustAllowsMailRead(ctx))
|
|
72
271
|
return "mail is private; this tool is only available in trusted contexts.";
|
|
272
|
+
const requestedScope = args.scope === "all" ? "all" : parseScope(args.scope);
|
|
273
|
+
if (requestedScope === "delegated" || requestedScope === "all") {
|
|
274
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
275
|
+
if (blocked)
|
|
276
|
+
return blocked;
|
|
277
|
+
}
|
|
73
278
|
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
74
279
|
if (!resolved.ok)
|
|
75
280
|
return resolved.error;
|
|
76
|
-
const scope =
|
|
281
|
+
const scope = requestedScope === "all"
|
|
282
|
+
? undefined
|
|
283
|
+
: requestedScope ?? (familyOrAgentSelf(ctx) ? undefined : "native");
|
|
77
284
|
const messages = await resolved.store.listMessages({
|
|
78
285
|
agentId: resolved.agentName,
|
|
79
|
-
placement: args.placement
|
|
286
|
+
placement: parsePlacement(args.placement),
|
|
80
287
|
compartmentKind: scope,
|
|
81
288
|
source: args.source,
|
|
82
289
|
limit: numberArg(args.limit, 10, 1, 20),
|
|
@@ -92,6 +299,123 @@ exports.mailToolDefinitions = [
|
|
|
92
299
|
},
|
|
93
300
|
summaryKeys: ["scope", "placement", "source", "limit"],
|
|
94
301
|
},
|
|
302
|
+
{
|
|
303
|
+
tool: {
|
|
304
|
+
type: "function",
|
|
305
|
+
function: {
|
|
306
|
+
name: "mail_compose",
|
|
307
|
+
description: "Create an outbound mail draft in the agent mailbox. This does not send mail; use mail_send with explicit confirmation for that.",
|
|
308
|
+
parameters: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
to: { type: "string", description: "Comma-separated recipient email addresses." },
|
|
312
|
+
cc: { type: "string", description: "Optional comma-separated CC addresses." },
|
|
313
|
+
bcc: { type: "string", description: "Optional comma-separated BCC addresses." },
|
|
314
|
+
subject: { type: "string", description: "Draft subject." },
|
|
315
|
+
text: { type: "string", description: "Plain-text draft body." },
|
|
316
|
+
reason: { type: "string", description: "Why this draft is being created. Logged for audit." },
|
|
317
|
+
},
|
|
318
|
+
required: ["to", "subject", "text", "reason"],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
handler: async (args, ctx) => {
|
|
323
|
+
if (!trustAllowsMailRead(ctx))
|
|
324
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
325
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
326
|
+
if (!resolved.ok)
|
|
327
|
+
return resolved.error;
|
|
328
|
+
try {
|
|
329
|
+
const draft = await (0, outbound_1.createMailDraft)({
|
|
330
|
+
store: resolved.store,
|
|
331
|
+
agentId: resolved.agentName,
|
|
332
|
+
from: resolved.config.mailboxAddress,
|
|
333
|
+
to: parseMailList(args.to),
|
|
334
|
+
cc: parseMailList(args.cc),
|
|
335
|
+
bcc: parseMailList(args.bcc),
|
|
336
|
+
subject: args.subject ?? "",
|
|
337
|
+
text: args.text ?? "",
|
|
338
|
+
actor: actorFromContext(ctx, resolved.agentName),
|
|
339
|
+
reason: args.reason ?? "compose outbound mail",
|
|
340
|
+
});
|
|
341
|
+
await resolved.store.recordAccess({
|
|
342
|
+
agentId: resolved.agentName,
|
|
343
|
+
tool: "mail_compose",
|
|
344
|
+
reason: args.reason || "compose outbound mail",
|
|
345
|
+
});
|
|
346
|
+
return [
|
|
347
|
+
`Draft created: ${draft.id}`,
|
|
348
|
+
`from: ${draft.from}`,
|
|
349
|
+
`to: ${draft.to.join(", ")}`,
|
|
350
|
+
`subject: ${draft.subject || "(no subject)"}`,
|
|
351
|
+
"send: call mail_send with draft_id and confirmation=CONFIRM_SEND after explicit approval.",
|
|
352
|
+
].join("\n");
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return error instanceof Error ? error.message : /* v8 ignore next -- defensive: draft creation throws Error instances. @preserve */ String(error);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
summaryKeys: ["to", "subject"],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
tool: {
|
|
362
|
+
type: "function",
|
|
363
|
+
function: {
|
|
364
|
+
name: "mail_send",
|
|
365
|
+
description: "Send a draft only after explicit confirmation. Autonomous sending is refused.",
|
|
366
|
+
parameters: {
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: {
|
|
369
|
+
draft_id: { type: "string", description: "Draft id from mail_compose." },
|
|
370
|
+
confirmation: { type: "string", description: "Must be exactly CONFIRM_SEND." },
|
|
371
|
+
reason: { type: "string", description: "Why this send is authorized. Logged for audit." },
|
|
372
|
+
autonomous: { type: "string", enum: ["true", "false"], description: "Must not be true; autonomous sends are refused." },
|
|
373
|
+
},
|
|
374
|
+
required: ["draft_id", "confirmation", "reason"],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
handler: async (args, ctx) => {
|
|
379
|
+
if (!trustAllowsMailRead(ctx))
|
|
380
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
381
|
+
const blocked = outboundSendBlocked(ctx);
|
|
382
|
+
if (blocked)
|
|
383
|
+
return blocked;
|
|
384
|
+
const draftId = (args.draft_id ?? "").trim();
|
|
385
|
+
if (!draftId)
|
|
386
|
+
return "draft_id is required.";
|
|
387
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
388
|
+
if (!resolved.ok)
|
|
389
|
+
return resolved.error;
|
|
390
|
+
try {
|
|
391
|
+
const sent = await (0, outbound_1.confirmMailDraftSend)({
|
|
392
|
+
store: resolved.store,
|
|
393
|
+
agentId: resolved.agentName,
|
|
394
|
+
draftId,
|
|
395
|
+
transport: (0, outbound_1.resolveOutboundTransport)(resolved.config),
|
|
396
|
+
confirmation: args.confirmation ?? "",
|
|
397
|
+
autonomous: args.autonomous === "true",
|
|
398
|
+
actor: actorFromContext(ctx, resolved.agentName),
|
|
399
|
+
reason: args.reason ?? "confirmed outbound send",
|
|
400
|
+
});
|
|
401
|
+
await resolved.store.recordAccess({
|
|
402
|
+
agentId: resolved.agentName,
|
|
403
|
+
tool: "mail_send",
|
|
404
|
+
reason: args.reason || "confirmed outbound send",
|
|
405
|
+
});
|
|
406
|
+
return [
|
|
407
|
+
`Mail sent: ${sent.id}`,
|
|
408
|
+
`transport: ${sent.transport}`,
|
|
409
|
+
`sentAt: ${sent.sentAt}`,
|
|
410
|
+
`to: ${sent.to.join(", ")}`,
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return error instanceof Error ? error.message : /* v8 ignore next -- defensive: send confirmation throws Error instances. @preserve */ String(error);
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
summaryKeys: ["draft_id"],
|
|
418
|
+
},
|
|
95
419
|
{
|
|
96
420
|
tool: {
|
|
97
421
|
type: "function",
|
|
@@ -103,6 +427,9 @@ exports.mailToolDefinitions = [
|
|
|
103
427
|
properties: {
|
|
104
428
|
query: { type: "string", description: "Search text." },
|
|
105
429
|
limit: { type: "string", description: "Maximum matching messages, 1-20. Defaults to 10." },
|
|
430
|
+
placement: { type: "string", enum: ["imbox", "screener", "discarded", "quarantine", "draft", "sent"], description: "Optional mailbox placement filter." },
|
|
431
|
+
scope: { type: "string", enum: ["native", "delegated", "all"], description: "Optional mailbox scope. Defaults to family/self-visible mail." },
|
|
432
|
+
source: { type: "string", description: "Optional delegated source filter, e.g. hey." },
|
|
106
433
|
reason: { type: "string", description: "Why you are searching this mail. Logged for audit." },
|
|
107
434
|
},
|
|
108
435
|
required: ["query"],
|
|
@@ -115,10 +442,24 @@ exports.mailToolDefinitions = [
|
|
|
115
442
|
const query = (args.query ?? "").trim().toLowerCase();
|
|
116
443
|
if (!query)
|
|
117
444
|
return "query is required.";
|
|
445
|
+
const requestedScope = args.scope === "all" ? "all" : parseScope(args.scope);
|
|
446
|
+
const explicitScope = (args.scope ?? "").trim().length > 0;
|
|
447
|
+
if (!familyOrAgentSelf(ctx) && explicitScope && requestedScope !== "native") {
|
|
448
|
+
return "delegated human mail requires family trust.";
|
|
449
|
+
}
|
|
118
450
|
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
119
451
|
if (!resolved.ok)
|
|
120
452
|
return resolved.error;
|
|
121
|
-
const
|
|
453
|
+
const scope = requestedScope === "all"
|
|
454
|
+
? undefined
|
|
455
|
+
: requestedScope ?? (familyOrAgentSelf(ctx) ? undefined : "native");
|
|
456
|
+
const all = await resolved.store.listMessages({
|
|
457
|
+
agentId: resolved.agentName,
|
|
458
|
+
placement: parsePlacement(args.placement),
|
|
459
|
+
compartmentKind: scope,
|
|
460
|
+
source: args.source,
|
|
461
|
+
limit: 200,
|
|
462
|
+
});
|
|
122
463
|
const matching = (0, file_store_1.decryptMessages)(all, resolved.config.privateKeys)
|
|
123
464
|
.filter((message) => [
|
|
124
465
|
message.private.subject,
|
|
@@ -167,6 +508,11 @@ exports.mailToolDefinitions = [
|
|
|
167
508
|
const message = await resolved.store.getMessage(messageId);
|
|
168
509
|
if (!message || message.agentId !== resolved.agentName)
|
|
169
510
|
return `No visible mail message found for ${messageId}.`;
|
|
511
|
+
if (message.compartmentKind === "delegated") {
|
|
512
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
513
|
+
if (blocked)
|
|
514
|
+
return blocked;
|
|
515
|
+
}
|
|
170
516
|
const decrypted = (0, file_store_1.decryptMessages)([message], resolved.config.privateKeys)[0];
|
|
171
517
|
await resolved.store.recordAccess({
|
|
172
518
|
agentId: resolved.agentName,
|
|
@@ -187,6 +533,133 @@ exports.mailToolDefinitions = [
|
|
|
187
533
|
},
|
|
188
534
|
summaryKeys: ["message_id", "reason"],
|
|
189
535
|
},
|
|
536
|
+
{
|
|
537
|
+
tool: {
|
|
538
|
+
type: "function",
|
|
539
|
+
function: {
|
|
540
|
+
name: "mail_screener",
|
|
541
|
+
description: "List Mail Screener candidates without message bodies so the agent can ask family how to resolve unknown inbound mail.",
|
|
542
|
+
parameters: {
|
|
543
|
+
type: "object",
|
|
544
|
+
properties: {
|
|
545
|
+
status: { type: "string", enum: ["pending", "allowed", "discarded", "quarantined", "restored"], description: "Optional Screener candidate status. Defaults to pending." },
|
|
546
|
+
placement: { type: "string", enum: ["screener", "discarded", "quarantine", "imbox"], description: "Optional current placement filter." },
|
|
547
|
+
limit: { type: "string", description: "Maximum candidates to return, 1-50. Defaults to 20." },
|
|
548
|
+
reason: { type: "string", description: "Why you are inspecting the Screener. Logged for audit." },
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
handler: async (args, ctx) => {
|
|
554
|
+
if (!trustAllowsMailRead(ctx))
|
|
555
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
556
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
557
|
+
if (blocked)
|
|
558
|
+
return blocked;
|
|
559
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
560
|
+
if (!resolved.ok)
|
|
561
|
+
return resolved.error;
|
|
562
|
+
const candidates = await resolved.store.listScreenerCandidates({
|
|
563
|
+
agentId: resolved.agentName,
|
|
564
|
+
status: parseCandidateStatus(args.status) ?? "pending",
|
|
565
|
+
placement: parsePlacement(args.placement),
|
|
566
|
+
limit: numberArg(args.limit, 20, 1, 50),
|
|
567
|
+
});
|
|
568
|
+
await resolved.store.recordAccess({
|
|
569
|
+
agentId: resolved.agentName,
|
|
570
|
+
tool: "mail_screener",
|
|
571
|
+
reason: args.reason || "screener overview",
|
|
572
|
+
});
|
|
573
|
+
if (candidates.length === 0)
|
|
574
|
+
return "No Screener candidates.";
|
|
575
|
+
return candidates.map(renderScreenerCandidate).join("\n\n");
|
|
576
|
+
},
|
|
577
|
+
summaryKeys: ["status", "placement", "limit"],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
tool: {
|
|
581
|
+
type: "function",
|
|
582
|
+
function: {
|
|
583
|
+
name: "mail_decide",
|
|
584
|
+
description: "Apply a family-authorized Screener decision to a candidate while retaining discarded mail for recovery.",
|
|
585
|
+
parameters: {
|
|
586
|
+
type: "object",
|
|
587
|
+
properties: {
|
|
588
|
+
candidate_id: { type: "string", description: "Candidate id from mail_screener." },
|
|
589
|
+
message_id: { type: "string", description: "Message id when resolving a known message directly." },
|
|
590
|
+
action: { type: "string", enum: ["link-friend", "create-friend", "allow-sender", "allow-source", "allow-domain", "allow-thread", "discard", "quarantine", "restore"], description: "Decision to apply." },
|
|
591
|
+
reason: { type: "string", description: "Why this decision is authorized. Logged for audit." },
|
|
592
|
+
friend_id: { type: "string", description: "Optional friend id for link-friend decisions." },
|
|
593
|
+
},
|
|
594
|
+
required: ["action", "reason"],
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
handler: async (args, ctx) => {
|
|
599
|
+
if (!trustAllowsMailRead(ctx))
|
|
600
|
+
return "mail is private; this tool is only available in trusted contexts.";
|
|
601
|
+
const blocked = screenerDecisionBlocked(ctx);
|
|
602
|
+
if (blocked)
|
|
603
|
+
return blocked;
|
|
604
|
+
const action = parseDecisionAction(args.action);
|
|
605
|
+
if (!action)
|
|
606
|
+
return "action is required and must be a supported mail decision.";
|
|
607
|
+
const reason = (args.reason ?? "").trim();
|
|
608
|
+
if (!reason)
|
|
609
|
+
return "reason is required.";
|
|
610
|
+
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
611
|
+
if (!resolved.ok)
|
|
612
|
+
return resolved.error;
|
|
613
|
+
let messageId = (args.message_id ?? "").trim();
|
|
614
|
+
const candidateId = (args.candidate_id ?? "").trim();
|
|
615
|
+
let candidate;
|
|
616
|
+
if (candidateId) {
|
|
617
|
+
const candidates = await resolved.store.listScreenerCandidates({ agentId: resolved.agentName, limit: 200 });
|
|
618
|
+
candidate = candidates.find((entry) => entry.id === candidateId);
|
|
619
|
+
if (!candidate)
|
|
620
|
+
return `No Screener candidate found for ${candidateId}.`;
|
|
621
|
+
messageId = candidate.messageId;
|
|
622
|
+
}
|
|
623
|
+
if (!messageId)
|
|
624
|
+
return "candidate_id or message_id is required.";
|
|
625
|
+
const message = await resolved.store.getMessage(messageId);
|
|
626
|
+
if (!message || message.agentId !== resolved.agentName)
|
|
627
|
+
return `No visible mail message found for ${messageId}.`;
|
|
628
|
+
const decision = await (0, policy_1.applyMailDecision)({
|
|
629
|
+
store: resolved.store,
|
|
630
|
+
agentId: resolved.agentName,
|
|
631
|
+
messageId,
|
|
632
|
+
action,
|
|
633
|
+
actor: actorFromContext(ctx, resolved.agentName),
|
|
634
|
+
reason,
|
|
635
|
+
...(args.friend_id ? { friendId: args.friend_id } : {}),
|
|
636
|
+
});
|
|
637
|
+
await resolved.store.recordAccess({
|
|
638
|
+
agentId: resolved.agentName,
|
|
639
|
+
messageId,
|
|
640
|
+
tool: "mail_decide",
|
|
641
|
+
reason,
|
|
642
|
+
});
|
|
643
|
+
const senderPolicyLine = persistSenderPolicyForDecision({
|
|
644
|
+
registryPath: resolved.config.registryPath,
|
|
645
|
+
agentId: resolved.agentName,
|
|
646
|
+
action,
|
|
647
|
+
reason,
|
|
648
|
+
actor: actorFromContext(ctx, resolved.agentName),
|
|
649
|
+
...(candidate ? { candidate } : {}),
|
|
650
|
+
message,
|
|
651
|
+
privateKeys: resolved.config.privateKeys,
|
|
652
|
+
});
|
|
653
|
+
return [
|
|
654
|
+
`Mail decision recorded: ${decision.action}`,
|
|
655
|
+
`message: ${decision.messageId}`,
|
|
656
|
+
`placement: ${decision.previousPlacement} -> ${decision.nextPlacement}`,
|
|
657
|
+
...(senderPolicyLine ? [senderPolicyLine] : []),
|
|
658
|
+
decision.nextPlacement === "discarded" ? "discarded mail remains retained in the recovery drawer." : `decision: ${decision.id}`,
|
|
659
|
+
].join("\n");
|
|
660
|
+
},
|
|
661
|
+
summaryKeys: ["candidate_id", "message_id", "action"],
|
|
662
|
+
},
|
|
190
663
|
{
|
|
191
664
|
tool: {
|
|
192
665
|
type: "function",
|
|
@@ -199,6 +672,9 @@ exports.mailToolDefinitions = [
|
|
|
199
672
|
handler: async (_args, ctx) => {
|
|
200
673
|
if (!trustAllowsMailRead(ctx))
|
|
201
674
|
return "mail is private; this tool is only available in trusted contexts.";
|
|
675
|
+
const blocked = delegatedHumanMailBlocked(ctx);
|
|
676
|
+
if (blocked)
|
|
677
|
+
return blocked;
|
|
202
678
|
const resolved = (0, reader_1.resolveMailroomReader)();
|
|
203
679
|
if (!resolved.ok)
|
|
204
680
|
return resolved.error;
|