@ouro.bot/cli 0.1.0-alpha.469 → 0.1.0-alpha.470

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 CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.470",
6
+ "changes": [
7
+ "Agent Mail native sending now enforces autonomy policy, confirmation fallback, recipient/rate limits, delegated send-as-human refusal, audit records, and kill switch state.",
8
+ "Outbound Mail now records provider submission and delivery state through ACS/Event Grid while keeping status summaries and Outlook audit output body-safe.",
9
+ "Mail recovery docs and `ouro doctor` now check mailbox identity, vault-held mail keys, hosted Blob reader coordinates, and autonomy kill switch state without treating generic vault item notes as machine contracts."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.469",
6
14
  "changes": [
@@ -89,6 +89,16 @@ function numberField(record, key, fallback) {
89
89
  const value = record?.[key];
90
90
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
91
91
  }
92
+ function hasStringRecordValue(value) {
93
+ const record = asRecord(value);
94
+ return !!record && Object.values(record).some((entry) => typeof entry === "string" && entry.trim().length > 0);
95
+ }
96
+ function mailAutonomyDetail(mailroom) {
97
+ const policy = asRecord(mailroom?.autonomousSendPolicy);
98
+ const autonomy = policy?.enabled === true ? "autonomy enabled" : "autonomy disabled";
99
+ const killSwitch = policy?.killSwitch === true ? "kill switch on" : "kill switch off";
100
+ return `${autonomy}; ${killSwitch}`;
101
+ }
92
102
  const SENSITIVE_CONFIG_KEYS = ["apiKey", "token", "secret", "password"];
93
103
  function credentialKeyLeaks(raw) {
94
104
  return SENSITIVE_CONFIG_KEYS.filter((key) => raw.includes(`"${key}"`));
@@ -256,6 +266,47 @@ async function checkSenses(deps) {
256
266
  });
257
267
  }
258
268
  }
269
+ if (sense === "mail" && senseObj.enabled === true) {
270
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
271
+ if (!runtimeConfig.ok) {
272
+ checks.push({
273
+ label: `${agentDir} mail config`,
274
+ status: "fail",
275
+ detail: `runtime config unavailable: ${runtimeConfig.error}`,
276
+ });
277
+ continue;
278
+ }
279
+ const mailroom = asRecord(runtimeConfig.config.mailroom);
280
+ const workSubstrate = asRecord(runtimeConfig.config.workSubstrate);
281
+ const mailboxAddress = textField(mailroom, "mailboxAddress");
282
+ const hosted = textField(workSubstrate, "mode") === "hosted";
283
+ const azureAccountUrl = textField(mailroom, "azureAccountUrl");
284
+ const azureContainer = textField(mailroom, "azureContainer") || "mailroom";
285
+ const missing = [];
286
+ if (!mailboxAddress)
287
+ missing.push("mailroom.mailboxAddress");
288
+ if (!hasStringRecordValue(mailroom?.privateKeys))
289
+ missing.push("mailroom.privateKeys");
290
+ if (hosted && !azureAccountUrl)
291
+ missing.push("mailroom.azureAccountUrl for hosted Blob reader");
292
+ if (missing.length > 0) {
293
+ checks.push({
294
+ label: `${agentDir} mail config`,
295
+ status: "fail",
296
+ detail: `missing ${missing.join("/")}`,
297
+ });
298
+ continue;
299
+ }
300
+ checks.push({
301
+ label: `${agentDir} mail config`,
302
+ status: "pass",
303
+ detail: [
304
+ mailboxAddress,
305
+ hosted ? `hosted azure-blob ${azureAccountUrl}/${azureContainer}` : "local file Mailroom",
306
+ mailAutonomyDetail(mailroom),
307
+ ].join("; "),
308
+ });
309
+ }
259
310
  }
260
311
  }
261
312
  if (checks.length === 0) {
@@ -88,7 +88,7 @@ function buildFolders(messages, outbound) {
88
88
  { id: "discarded", label: "Discarded", count: messages.filter((message) => message.placement === "discarded").length },
89
89
  { id: "quarantine", label: "Quarantine", count: messages.filter((message) => message.placement === "quarantine").length },
90
90
  { id: "draft", label: "Drafts", count: outbound.filter((record) => record.status === "draft").length },
91
- { id: "sent", label: "Sent", count: outbound.filter((record) => record.status === "sent").length },
91
+ { id: "sent", label: "Sent", count: outbound.filter((record) => record.status !== "draft").length },
92
92
  { id: "delegated", label: "Delegated", count: messages.filter((message) => message.compartmentKind === "delegated").length },
93
93
  { id: "native", label: "Native", count: messages.filter((message) => message.compartmentKind === "native").length },
94
94
  ];
@@ -135,6 +135,7 @@ function screenerCandidate(candidate) {
135
135
  };
136
136
  }
137
137
  function outboundRecord(record) {
138
+ const policyDecision = record.policyDecision;
138
139
  return {
139
140
  id: record.id,
140
141
  status: record.status,
@@ -150,6 +151,39 @@ function outboundRecord(record) {
150
151
  createdAt: record.createdAt,
151
152
  updatedAt: record.updatedAt,
152
153
  sentAt: record.sentAt ?? null,
154
+ submittedAt: record.submittedAt ?? null,
155
+ acceptedAt: record.acceptedAt ?? null,
156
+ deliveredAt: record.deliveredAt ?? null,
157
+ failedAt: record.failedAt ?? null,
158
+ sendMode: record.sendMode ?? null,
159
+ policyDecision: policyDecision
160
+ ? {
161
+ allowed: policyDecision.allowed,
162
+ mode: policyDecision.mode,
163
+ code: policyDecision.code,
164
+ reason: policyDecision.reason,
165
+ evaluatedAt: policyDecision.evaluatedAt,
166
+ recipients: policyDecision.recipients,
167
+ fallback: policyDecision.fallback,
168
+ policyId: policyDecision.policyId ?? null,
169
+ remainingSendsInWindow: policyDecision.remainingSendsInWindow ?? null,
170
+ }
171
+ : null,
172
+ provider: record.provider ?? null,
173
+ providerMessageId: record.providerMessageId ?? null,
174
+ providerRequestId: record.providerRequestId ?? null,
175
+ operationLocation: record.operationLocation ?? null,
176
+ deliveryEvents: (record.deliveryEvents ?? []).map((event) => ({
177
+ provider: event.provider,
178
+ providerEventId: event.providerEventId,
179
+ providerMessageId: event.providerMessageId,
180
+ outcome: event.outcome,
181
+ recipient: event.recipient ?? null,
182
+ occurredAt: event.occurredAt,
183
+ receivedAt: event.receivedAt,
184
+ bodySafeSummary: event.bodySafeSummary,
185
+ providerStatus: event.providerStatus ?? null,
186
+ })),
153
187
  transport: record.transport ?? null,
154
188
  reason: record.reason,
155
189
  };
@@ -0,0 +1,209 @@
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.buildNativeMailAutonomyPolicy = buildNativeMailAutonomyPolicy;
37
+ exports.evaluateNativeMailSendPolicy = evaluateNativeMailSendPolicy;
38
+ exports.buildConfirmedMailSendDecision = buildConfirmedMailSendDecision;
39
+ const crypto = __importStar(require("node:crypto"));
40
+ const runtime_1 = require("../nerves/runtime");
41
+ const core_1 = require("./core");
42
+ function stableJson(value) {
43
+ if (Array.isArray(value))
44
+ return `[${value.map(stableJson).join(",")}]`;
45
+ if (value && typeof value === "object") {
46
+ const record = value;
47
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
48
+ }
49
+ return JSON.stringify(value);
50
+ }
51
+ function safeAddressPart(value) {
52
+ return value
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]+/g, "-")
55
+ .replace(/^-+|-+$/g, "");
56
+ }
57
+ function normalizeDomain(value) {
58
+ return value.trim().toLowerCase().replace(/^@/, "");
59
+ }
60
+ function autonomyPolicyId(input) {
61
+ return `mail_auto_${crypto.createHash("sha256").update(stableJson(input)).digest("hex").slice(0, 16)}`;
62
+ }
63
+ function recipientsForDecision(record) {
64
+ return [...record.to, ...record.cc, ...record.bcc].map(core_1.normalizeMailAddress);
65
+ }
66
+ function decision(input) {
67
+ return { schemaVersion: 1, ...input };
68
+ }
69
+ function recipientDomain(recipient) {
70
+ return recipient.slice(recipient.indexOf("@") + 1).toLowerCase();
71
+ }
72
+ function isRecipientAllowed(policy, recipient) {
73
+ return policy.allowedRecipients.includes(recipient) || policy.allowedDomains.includes(recipientDomain(recipient));
74
+ }
75
+ function autonomousSentAt(record) {
76
+ if (record.status !== "sent" || record.sendMode !== "autonomous")
77
+ return null;
78
+ return record.sentAt ?? record.updatedAt;
79
+ }
80
+ function countRecentAutonomousSends(input) {
81
+ const startsAt = input.nowMs - input.windowMs;
82
+ return input.recentOutbound.filter((record) => {
83
+ const sentAt = autonomousSentAt(record);
84
+ if (!sentAt)
85
+ return false;
86
+ const sentMs = Date.parse(sentAt);
87
+ return Number.isFinite(sentMs) && sentMs >= startsAt && sentMs <= input.nowMs;
88
+ }).length;
89
+ }
90
+ function buildNativeMailAutonomyPolicy(input) {
91
+ const normalized = {
92
+ agentId: safeAddressPart(input.agentId) || "agent",
93
+ mailboxAddress: (0, core_1.normalizeMailAddress)(input.mailboxAddress),
94
+ enabled: input.enabled,
95
+ killSwitch: input.killSwitch,
96
+ allowedRecipients: [...new Set((input.allowedRecipients ?? []).map(core_1.normalizeMailAddress))].sort(),
97
+ allowedDomains: [...new Set((input.allowedDomains ?? []).map(normalizeDomain).filter(Boolean))].sort(),
98
+ maxRecipientsPerMessage: Math.max(1, Math.floor(input.maxRecipientsPerMessage)),
99
+ rateLimit: {
100
+ maxSends: Math.max(0, Math.floor(input.rateLimit.maxSends)),
101
+ windowMs: Math.max(1, Math.floor(input.rateLimit.windowMs)),
102
+ },
103
+ ...(input.actor ? { actor: input.actor } : {}),
104
+ ...(input.reason ? { reason: input.reason } : {}),
105
+ ...(input.updatedAt ? { updatedAt: input.updatedAt } : {}),
106
+ };
107
+ const policy = {
108
+ schemaVersion: 1,
109
+ policyId: autonomyPolicyId(normalized),
110
+ ...normalized,
111
+ };
112
+ (0, runtime_1.emitNervesEvent)({
113
+ component: "senses",
114
+ event: "senses.mail_native_autonomy_policy_built",
115
+ message: "native mail autonomy policy built",
116
+ meta: { agentId: policy.agentId, policyId: policy.policyId, enabled: policy.enabled, killSwitch: policy.killSwitch },
117
+ });
118
+ return policy;
119
+ }
120
+ function evaluateNativeMailSendPolicy(input) {
121
+ const now = input.now ?? new Date();
122
+ const evaluatedAt = now.toISOString();
123
+ const recipients = recipientsForDecision(input.draft);
124
+ const policyId = input.policy.policyId;
125
+ const blocked = (code, reason, mode = "blocked", fallback = "none") => decision({
126
+ allowed: false,
127
+ mode,
128
+ code,
129
+ reason,
130
+ evaluatedAt,
131
+ recipients,
132
+ fallback,
133
+ policyId,
134
+ });
135
+ if (input.draft.status !== "draft") {
136
+ return blocked("draft-not-sendable", `Draft ${input.draft.id} is already ${input.draft.status}`);
137
+ }
138
+ if (input.draft.mailboxRole === "delegated-human-mailbox" || input.draft.ownerEmail || input.draft.source || input.draft.sendAuthority !== "agent-native") {
139
+ return blocked("delegated-send-as-human-not-authorized", "Delegated human mail does not grant send-as-human authority");
140
+ }
141
+ if (safeAddressPart(input.draft.agentId) !== input.policy.agentId) {
142
+ return blocked("agent-mismatch", `Draft belongs to ${input.draft.agentId}, not ${input.policy.agentId}`);
143
+ }
144
+ if ((0, core_1.normalizeMailAddress)(input.draft.from) !== input.policy.mailboxAddress) {
145
+ return blocked("native-mailbox-mismatch", `${input.draft.from} is not the native mailbox ${input.policy.mailboxAddress}`);
146
+ }
147
+ if (!input.policy.enabled) {
148
+ return blocked("autonomy-policy-disabled", "Autonomous native-agent mail policy is disabled", "confirmation-required", "CONFIRM_SEND");
149
+ }
150
+ if (input.policy.killSwitch) {
151
+ return blocked("autonomy-kill-switch", "Autonomous native-agent mail kill switch is enabled", "confirmation-required", "CONFIRM_SEND");
152
+ }
153
+ if (recipients.length > input.policy.maxRecipientsPerMessage) {
154
+ return blocked("recipient-limit-exceeded", `Autonomous native-agent mail is limited to ${input.policy.maxRecipientsPerMessage} recipient(s)`);
155
+ }
156
+ const unallowed = recipients.find((recipient) => !isRecipientAllowed(input.policy, recipient));
157
+ if (unallowed) {
158
+ return blocked("recipient-not-allowed", `${unallowed} is not allowed for autonomous native-agent mail`, "confirmation-required", "CONFIRM_SEND");
159
+ }
160
+ const recentCount = countRecentAutonomousSends({
161
+ recentOutbound: input.recentOutbound,
162
+ nowMs: now.getTime(),
163
+ windowMs: input.policy.rateLimit.windowMs,
164
+ });
165
+ if (recentCount >= input.policy.rateLimit.maxSends) {
166
+ return blocked("autonomous-rate-limit", "Autonomous native-agent mail rate limit is exhausted");
167
+ }
168
+ const allowed = decision({
169
+ allowed: true,
170
+ mode: "autonomous",
171
+ code: "allowed",
172
+ reason: "Autonomous native-agent mail policy allowed this send",
173
+ evaluatedAt,
174
+ recipients,
175
+ fallback: "none",
176
+ policyId,
177
+ remainingSendsInWindow: Math.max(0, input.policy.rateLimit.maxSends - recentCount - 1),
178
+ });
179
+ (0, runtime_1.emitNervesEvent)({
180
+ component: "senses",
181
+ event: "senses.mail_native_autonomy_allowed",
182
+ message: "native mail autonomy policy allowed send",
183
+ meta: { agentId: input.policy.agentId, policyId, recipientCount: recipients.length },
184
+ });
185
+ return allowed;
186
+ }
187
+ function buildConfirmedMailSendDecision(input) {
188
+ const decisionValue = decision({
189
+ allowed: true,
190
+ mode: "confirmed",
191
+ code: "explicit-confirmation",
192
+ reason: "Explicit confirmation authorized this native-agent send",
193
+ evaluatedAt: (input.now ?? new Date()).toISOString(),
194
+ recipients: recipientsForDecision(input.draft),
195
+ fallback: "none",
196
+ ...(input.policy ? { policyId: input.policy.policyId } : {}),
197
+ });
198
+ (0, runtime_1.emitNervesEvent)({
199
+ component: "senses",
200
+ event: "senses.mail_native_send_confirmed",
201
+ message: "native mail send confirmed",
202
+ meta: {
203
+ agentId: input.draft.agentId,
204
+ recipientCount: decisionValue.recipients.length,
205
+ ...(input.policy ? { policyId: input.policy.policyId } : {}),
206
+ },
207
+ });
208
+ return decisionValue;
209
+ }
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.normalizeMailAddress = normalizeMailAddress;
37
+ exports.buildMailProviderSubmission = buildMailProviderSubmission;
38
+ exports.parseAcsEmailDeliveryReportEvent = parseAcsEmailDeliveryReportEvent;
39
+ exports.reconcileMailDeliveryEvent = reconcileMailDeliveryEvent;
37
40
  exports.reverseEmailRoute = reverseEmailRoute;
38
41
  exports.sourceAliasForOwner = sourceAliasForOwner;
39
42
  exports.generateMailKeyPair = generateMailKeyPair;
@@ -79,6 +82,91 @@ function normalizeMailAddress(address) {
79
82
  }
80
83
  return normalized;
81
84
  }
85
+ function buildMailProviderSubmission(input) {
86
+ return {
87
+ ...input.draft,
88
+ status: "submitted",
89
+ provider: input.provider,
90
+ providerMessageId: input.providerMessageId,
91
+ ...(input.providerRequestId ? { providerRequestId: input.providerRequestId } : {}),
92
+ ...(input.operationLocation ? { operationLocation: input.operationLocation } : {}),
93
+ submittedAt: input.submittedAt,
94
+ updatedAt: input.submittedAt,
95
+ deliveryEvents: [],
96
+ };
97
+ }
98
+ function recordField(value, key) {
99
+ return value && typeof value === "object" && !Array.isArray(value)
100
+ ? value[key]
101
+ : undefined;
102
+ }
103
+ function stringField(value, key) {
104
+ const field = recordField(value, key);
105
+ return typeof field === "string" ? field : "";
106
+ }
107
+ function acsOutcome(status) {
108
+ switch (status) {
109
+ case "Delivered": return "delivered";
110
+ case "Suppressed": return "suppressed";
111
+ case "Bounced": return "bounced";
112
+ case "Quarantined": return "quarantined";
113
+ case "FilteredSpam": return "spam-filtered";
114
+ case "Expanded": return "accepted";
115
+ case "Failed": return "failed";
116
+ default: throw new Error(`unsupported ACS delivery status: ${status || "unknown"}`);
117
+ }
118
+ }
119
+ function parseAcsEmailDeliveryReportEvent(event) {
120
+ const providerEventId = stringField(event, "id");
121
+ const eventType = stringField(event, "eventType");
122
+ const data = recordField(event, "data");
123
+ if (!providerEventId)
124
+ throw new Error("ACS delivery event is missing id");
125
+ if (eventType !== "Microsoft.Communication.EmailDeliveryReportReceived") {
126
+ throw new Error(`unsupported ACS event type: ${eventType || "unknown"}`);
127
+ }
128
+ const providerMessageId = stringField(data, "messageId");
129
+ const status = stringField(data, "status");
130
+ if (!providerMessageId)
131
+ throw new Error("ACS delivery event is missing messageId");
132
+ const recipient = stringField(data, "recipient");
133
+ const eventTime = stringField(event, "eventTime");
134
+ const occurredAt = stringField(data, "deliveryAttemptTimeStamp") || eventTime || new Date().toISOString();
135
+ const normalizedRecipient = recipient ? normalizeMailAddress(recipient) : "";
136
+ return {
137
+ schemaVersion: 1,
138
+ provider: "azure-communication-services",
139
+ providerEventId,
140
+ providerMessageId,
141
+ outcome: acsOutcome(status),
142
+ ...(normalizedRecipient ? { recipient: normalizedRecipient } : {}),
143
+ occurredAt,
144
+ receivedAt: eventTime || occurredAt,
145
+ bodySafeSummary: `ACS delivery report ${status} for ${normalizedRecipient || "unknown recipient"}`,
146
+ providerStatus: status,
147
+ };
148
+ }
149
+ function reconcileMailDeliveryEvent(input) {
150
+ if (input.outbound.providerMessageId && input.outbound.providerMessageId !== input.event.providerMessageId) {
151
+ throw new Error("delivery event providerMessageId does not match outbound record");
152
+ }
153
+ const existingEvents = input.outbound.deliveryEvents ?? [];
154
+ if (existingEvents.some((event) => event.providerEventId === input.event.providerEventId)) {
155
+ return input.outbound;
156
+ }
157
+ const timestampKey = input.event.outcome === "delivered"
158
+ ? "deliveredAt"
159
+ : input.event.outcome === "accepted"
160
+ ? "acceptedAt"
161
+ : "failedAt";
162
+ return {
163
+ ...input.outbound,
164
+ status: input.event.outcome,
165
+ updatedAt: input.event.occurredAt,
166
+ deliveryEvents: [...existingEvents, input.event],
167
+ [timestampKey]: input.event.occurredAt,
168
+ };
169
+ }
82
170
  function safeAddressPart(value) {
83
171
  return value
84
172
  .toLowerCase()