@ouro.bot/cli 0.1.0-alpha.453 → 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.
@@ -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-BXw3xmUo.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-D4Wg-o8Z.css">
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 Imbox/Screener filter." },
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 = args.scope === "native" || args.scope === "delegated" ? args.scope : undefined;
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 === "imbox" || args.placement === "screener" ? args.placement : undefined,
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 all = await resolved.store.listMessages({ agentId: resolved.agentName, limit: 200 });
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;