@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.
@@ -33,15 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseAcsEmailDeliveryReportEvent = void 0;
37
+ exports.createAcsEmailProviderClient = createAcsEmailProviderClient;
36
38
  exports.resolveOutboundTransport = resolveOutboundTransport;
37
39
  exports.createMailDraft = createMailDraft;
38
40
  exports.confirmMailDraftSend = confirmMailDraftSend;
39
41
  exports.listMailOutboundRecords = listMailOutboundRecords;
42
+ exports.reconcileOutboundDeliveryEvent = reconcileOutboundDeliveryEvent;
40
43
  const crypto = __importStar(require("node:crypto"));
41
44
  const fs = __importStar(require("node:fs"));
42
45
  const path = __importStar(require("node:path"));
43
46
  const runtime_1 = require("../nerves/runtime");
47
+ const autonomy_1 = require("./autonomy");
44
48
  const core_1 = require("./core");
49
+ Object.defineProperty(exports, "parseAcsEmailDeliveryReportEvent", { enumerable: true, get: function () { return core_1.parseAcsEmailDeliveryReportEvent; } });
45
50
  function isRecord(value) {
46
51
  return !!value && typeof value === "object" && !Array.isArray(value);
47
52
  }
@@ -49,12 +54,100 @@ function textField(value, key) {
49
54
  const raw = value[key];
50
55
  return typeof raw === "string" ? raw.trim() : "";
51
56
  }
57
+ function credentialFields(value) {
58
+ if (!isRecord(value))
59
+ return undefined;
60
+ const accessKey = textField(value, "accessKey");
61
+ const connectionString = textField(value, "connectionString");
62
+ const fields = {
63
+ ...(accessKey ? { accessKey } : {}),
64
+ ...(connectionString ? { connectionString } : {}),
65
+ };
66
+ return Object.keys(fields).length > 0 ? fields : undefined;
67
+ }
52
68
  function normalizeList(values) {
53
69
  return values
54
70
  .map((value) => value.trim())
55
71
  .filter(Boolean)
56
72
  .map(core_1.normalizeMailAddress);
57
73
  }
74
+ function contentHash(body) {
75
+ return crypto.createHash("sha256").update(body, "utf-8").digest("base64");
76
+ }
77
+ function hmacSignature(input) {
78
+ const stringToSign = `${input.method}\n${input.pathAndQuery}\n${input.date};${input.host};${input.contentHash}`;
79
+ return crypto.createHmac("sha256", Buffer.from(input.accessKey, "base64")).update(stringToSign, "utf-8").digest("base64");
80
+ }
81
+ function recipientObjects(addresses) {
82
+ return addresses.map((address) => ({ address }));
83
+ }
84
+ function providerMessageIdFromOperationLocation(operationLocation) {
85
+ const match = operationLocation.match(/\/operations\/([^?/#]+)/);
86
+ return match?.[1] ?? "";
87
+ }
88
+ function createAcsEmailProviderClient(input) {
89
+ const endpoint = input.endpoint.replace(/\/+$/, "");
90
+ const fetchImpl = input.fetch ?? fetch;
91
+ return {
92
+ async submit(submitInput) {
93
+ const url = new URL(`${endpoint}/emails:send?api-version=2025-09-01`);
94
+ const senderAddress = submitInput.transport.senderAddress ?? submitInput.draft.from;
95
+ const body = JSON.stringify({
96
+ senderAddress,
97
+ recipients: {
98
+ to: recipientObjects(submitInput.draft.to),
99
+ cc: recipientObjects(submitInput.draft.cc),
100
+ bcc: recipientObjects(submitInput.draft.bcc),
101
+ },
102
+ content: {
103
+ subject: submitInput.draft.subject,
104
+ plainText: submitInput.draft.text,
105
+ },
106
+ });
107
+ const date = (input.now ?? (() => new Date()))().toUTCString();
108
+ const hash = contentHash(body);
109
+ const signature = hmacSignature({
110
+ method: "POST",
111
+ pathAndQuery: `${url.pathname}${url.search}`,
112
+ date,
113
+ host: url.host,
114
+ contentHash: hash,
115
+ accessKey: input.accessKey,
116
+ });
117
+ const response = await fetchImpl(url.toString(), {
118
+ method: "POST",
119
+ headers: {
120
+ "content-type": "application/json",
121
+ "x-ms-date": date,
122
+ "x-ms-content-sha256": hash,
123
+ authorization: `HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=${signature}`,
124
+ },
125
+ body,
126
+ });
127
+ const operationLocation = response.headers.get("operation-location") ?? undefined;
128
+ const providerRequestId = response.headers.get("x-ms-request-id") ?? undefined;
129
+ const payload = await response.json().catch(() => ({}));
130
+ if (!response.ok) {
131
+ const reason = typeof payload.error?.message === "string" ? payload.error.message : `HTTP ${response.status}`;
132
+ throw new Error(`ACS outbound send failed: ${reason}`);
133
+ }
134
+ const providerMessageId = typeof payload.id === "string"
135
+ ? payload.id
136
+ : operationLocation
137
+ ? providerMessageIdFromOperationLocation(operationLocation)
138
+ : "";
139
+ if (!providerMessageId)
140
+ throw new Error("ACS outbound send did not return an operation id");
141
+ return {
142
+ provider: "azure-communication-services",
143
+ providerMessageId,
144
+ ...(operationLocation ? { operationLocation } : {}),
145
+ ...(providerRequestId ? { providerRequestId } : {}),
146
+ submittedAt: submitInput.submittedAt,
147
+ };
148
+ },
149
+ };
150
+ }
58
151
  function draftId() {
59
152
  return `draft_${crypto.randomBytes(12).toString("hex")}`;
60
153
  }
@@ -75,14 +168,21 @@ function resolveOutboundTransport(config) {
75
168
  return { kind: "local-sink", sinkPath };
76
169
  }
77
170
  if (transport === "azure-communication-services") {
171
+ if ("credentialItemNoteQuery" in outbound || "noteQuery" in outbound || "notes" in outbound) {
172
+ throw new Error("outbound provider binding must not infer credentials from vault notes");
173
+ }
78
174
  const endpoint = textField(outbound, "endpoint");
79
175
  if (!endpoint)
80
176
  throw new Error("outbound Azure Communication Services transport is missing endpoint");
81
177
  const senderAddress = textField(outbound, "senderAddress");
178
+ const credentialItem = textField(outbound, "credentialItem");
179
+ const fields = credentialFields(outbound.credentialFields);
82
180
  return {
83
181
  kind: "azure-communication-services",
84
182
  endpoint,
85
183
  ...(senderAddress ? { senderAddress } : {}),
184
+ ...(credentialItem ? { credentialItem } : {}),
185
+ ...(fields ? { credentialFields: fields } : {}),
86
186
  };
87
187
  }
88
188
  throw new Error("outbound mail transport is not configured; human-required: choose local-sink or azure-communication-services");
@@ -134,36 +234,94 @@ function appendLocalSink(transport, record, sentAt) {
134
234
  bcc: record.bcc,
135
235
  subject: record.subject,
136
236
  text: record.text,
237
+ sendMode: record.sendMode,
238
+ policyId: record.policyDecision?.policyId ?? null,
137
239
  sentAt,
138
240
  })}\n`, "utf-8");
139
241
  return transportMessageId;
140
242
  }
141
243
  function transportSend(transport, record, sentAt) {
142
- if (transport.kind === "local-sink")
143
- return appendLocalSink(transport, record, sentAt);
144
- throw new Error("Azure Communication Services outbound send is configured but not enabled on this machine; human-required setup is still needed");
244
+ return appendLocalSink(transport, record, sentAt);
145
245
  }
146
246
  async function confirmMailDraftSend(input) {
147
- if (input.autonomous) {
148
- throw new Error("Autonomous mail sending is disabled; create a draft and require explicit confirmation instead");
149
- }
150
- if (input.confirmation !== "CONFIRM_SEND") {
151
- throw new Error("mail_send requires confirmation=CONFIRM_SEND before any outbound mail leaves the agent");
152
- }
153
247
  const draft = await input.store.getMailOutbound(input.draftId);
154
248
  if (!draft || draft.agentId !== input.agentId)
155
249
  throw new Error(`No draft found for ${input.draftId}`);
156
250
  if (draft.status !== "draft")
157
251
  throw new Error(`Draft ${input.draftId} is already ${draft.status}`);
158
252
  const sentAt = (input.now ?? (() => new Date()))().toISOString();
159
- const transportMessageId = transportSend(input.transport, draft, sentAt);
160
- const sent = {
253
+ const recentOutbound = await input.store.listMailOutbound(input.agentId);
254
+ const policyDecision = input.autonomous
255
+ ? (() => {
256
+ if (!input.autonomyPolicy) {
257
+ throw new Error("Autonomous mail sending requires an enabled native-agent policy");
258
+ }
259
+ const decision = (0, autonomy_1.evaluateNativeMailSendPolicy)({
260
+ policy: input.autonomyPolicy,
261
+ draft,
262
+ recentOutbound,
263
+ now: new Date(sentAt),
264
+ });
265
+ if (!decision.allowed) {
266
+ if (decision.mode === "confirmation-required") {
267
+ throw new Error(`Autonomous mail send ${decision.code} requires confirmation=CONFIRM_SEND: ${decision.reason}`);
268
+ }
269
+ throw new Error(`${decision.code}: ${decision.reason}`);
270
+ }
271
+ return decision;
272
+ })()
273
+ : (() => {
274
+ if (input.confirmation !== "CONFIRM_SEND") {
275
+ throw new Error("mail_send requires confirmation=CONFIRM_SEND before any outbound mail leaves the agent");
276
+ }
277
+ return (0, autonomy_1.buildConfirmedMailSendDecision)({
278
+ draft,
279
+ policy: input.autonomyPolicy,
280
+ now: new Date(sentAt),
281
+ });
282
+ })();
283
+ const pendingSent = {
161
284
  ...draft,
162
- status: "sent",
285
+ status: input.transport.kind === "local-sink" ? "sent" : "submitted",
163
286
  actor: input.actor,
164
287
  reason: input.reason,
165
288
  updatedAt: sentAt,
289
+ sendMode: input.autonomous ? "autonomous" : "confirmed",
290
+ policyDecision,
166
291
  sentAt,
292
+ };
293
+ if (input.transport.kind === "azure-communication-services") {
294
+ if (!input.providerClient) {
295
+ throw new Error("Azure Communication Services outbound send is configured but not enabled on this machine; human-required setup is still needed");
296
+ }
297
+ const submission = await input.providerClient.submit({
298
+ draft: pendingSent,
299
+ transport: input.transport,
300
+ submittedAt: sentAt,
301
+ });
302
+ const submitted = {
303
+ ...(0, core_1.buildMailProviderSubmission)({
304
+ draft: pendingSent,
305
+ provider: submission.provider,
306
+ providerMessageId: submission.providerMessageId,
307
+ submittedAt: submission.submittedAt ?? sentAt,
308
+ ...(submission.operationLocation ? { operationLocation: submission.operationLocation } : {}),
309
+ ...(submission.providerRequestId ? { providerRequestId: submission.providerRequestId } : {}),
310
+ }),
311
+ transport: input.transport.kind,
312
+ };
313
+ await input.store.upsertMailOutbound(submitted);
314
+ (0, runtime_1.emitNervesEvent)({
315
+ component: "senses",
316
+ event: "senses.mail_draft_submitted",
317
+ message: "mail draft submitted to outbound provider",
318
+ meta: { agentId: submitted.agentId, id: submitted.id, provider: submitted.provider, providerMessageId: submitted.providerMessageId },
319
+ });
320
+ return submitted;
321
+ }
322
+ const transportMessageId = transportSend(input.transport, pendingSent, sentAt);
323
+ const sent = {
324
+ ...pendingSent,
167
325
  transport: input.transport.kind,
168
326
  transportMessageId,
169
327
  };
@@ -179,3 +337,24 @@ async function confirmMailDraftSend(input) {
179
337
  function listMailOutboundRecords(store, agentId) {
180
338
  return store.listMailOutbound(agentId);
181
339
  }
340
+ async function reconcileOutboundDeliveryEvent(input) {
341
+ const records = await input.store.listMailOutbound(input.agentId);
342
+ const outbound = records.find((record) => record.providerMessageId === input.event.providerMessageId);
343
+ if (!outbound)
344
+ throw new Error(`No outbound record found for provider message ${input.event.providerMessageId}`);
345
+ const updated = (0, core_1.reconcileMailDeliveryEvent)({ outbound, event: input.event });
346
+ await input.store.upsertMailOutbound(updated);
347
+ (0, runtime_1.emitNervesEvent)({
348
+ component: "senses",
349
+ event: "senses.mail_delivery_event_reconciled",
350
+ message: "mail delivery event reconciled",
351
+ meta: {
352
+ agentId: updated.agentId,
353
+ id: updated.id,
354
+ provider: input.event.provider,
355
+ providerEventId: input.event.providerEventId,
356
+ outcome: input.event.outcome,
357
+ },
358
+ });
359
+ return updated;
360
+ }
@@ -77,6 +77,9 @@ function parseMailroomConfig(value) {
77
77
  const host = textField(value, "host");
78
78
  const attentionIntervalMs = numberField(value, "attentionIntervalMs");
79
79
  const outbound = isRecord(value.outbound) ? { ...value.outbound } : undefined;
80
+ const autonomousSendPolicy = isRecord(value.autonomousSendPolicy)
81
+ ? { ...value.autonomousSendPolicy }
82
+ : undefined;
80
83
  return {
81
84
  mailboxAddress,
82
85
  ...(registryPath ? { registryPath } : {}),
@@ -89,6 +92,7 @@ function parseMailroomConfig(value) {
89
92
  ...(host ? { host } : {}),
90
93
  ...(attentionIntervalMs !== undefined ? { attentionIntervalMs } : {}),
91
94
  ...(outbound ? { outbound } : {}),
95
+ ...(autonomousSendPolicy ? { autonomousSendPolicy } : {}),
92
96
  privateKeys,
93
97
  };
94
98
  }