@ouro.bot/cli 0.1.0-alpha.471 → 0.1.0-alpha.473
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 +15 -0
- package/dist/mailroom/outbound.js +20 -0
- package/dist/repertoire/tools-mail.js +108 -7
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
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.473",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`mail_send` now resolves Azure Communication Services outbound provider clients from explicit generic vault-item bindings, so production ACS sends can leave the agent mailbox after `CONFIRM_SEND` without parsing vault notes.",
|
|
8
|
+
"Outbound provider bindings now require an explicit credential item and secret field for ACS access keys, with tests covering missing, blank, malformed, non-object, and legacy-shaped vault payloads without leaking secret values.",
|
|
9
|
+
"The `ouro.bot` wrapper stays version-synced for the outbound provider-client release."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.472",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Mail tools now stay usable when a visible message was encrypted to a missing old private key: `mail_recent`, `mail_search`, and `mail_thread` skip or explain only the undecryptable record with body-safe message/key ids.",
|
|
16
|
+
"Agent Mail recovery docs now distinguish restoring an old private key from hosted key rotation, so future agents do not repeatedly rotate when only an already-lost message is affected."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.471",
|
|
6
21
|
"changes": [
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.parseAcsEmailDeliveryReportEvent = void 0;
|
|
37
37
|
exports.createAcsEmailProviderClient = createAcsEmailProviderClient;
|
|
38
|
+
exports.resolveOutboundProviderClient = resolveOutboundProviderClient;
|
|
38
39
|
exports.resolveOutboundTransport = resolveOutboundTransport;
|
|
39
40
|
exports.createMailDraft = createMailDraft;
|
|
40
41
|
exports.confirmMailDraftSend = confirmMailDraftSend;
|
|
@@ -148,6 +149,25 @@ function createAcsEmailProviderClient(input) {
|
|
|
148
149
|
},
|
|
149
150
|
};
|
|
150
151
|
}
|
|
152
|
+
async function resolveOutboundProviderClient(transport, reader, options = {}) {
|
|
153
|
+
if (transport.kind !== "azure-communication-services")
|
|
154
|
+
return undefined;
|
|
155
|
+
const credentialItem = transport.credentialItem?.trim();
|
|
156
|
+
if (!credentialItem) {
|
|
157
|
+
throw new Error("outbound Azure Communication Services transport is missing credentialItem");
|
|
158
|
+
}
|
|
159
|
+
const accessKeyField = transport.credentialFields?.accessKey?.trim() || "accessKey";
|
|
160
|
+
const accessKey = (await reader.readSecretField(credentialItem, accessKeyField)).trim();
|
|
161
|
+
if (!accessKey) {
|
|
162
|
+
throw new Error(`outbound Azure Communication Services required secret field ${accessKeyField} is blank`);
|
|
163
|
+
}
|
|
164
|
+
return createAcsEmailProviderClient({
|
|
165
|
+
endpoint: transport.endpoint,
|
|
166
|
+
accessKey,
|
|
167
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
168
|
+
...(options.now ? { now: options.now } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
151
171
|
function draftId() {
|
|
152
172
|
return `draft_${crypto.randomBytes(12).toString("hex")}`;
|
|
153
173
|
}
|
|
@@ -42,6 +42,7 @@ const outbound_1 = require("../mailroom/outbound");
|
|
|
42
42
|
const policy_1 = require("../mailroom/policy");
|
|
43
43
|
const core_1 = require("../mailroom/core");
|
|
44
44
|
const runtime_1 = require("../nerves/runtime");
|
|
45
|
+
const credential_access_1 = require("./credential-access");
|
|
45
46
|
function trustAllowsMailRead(ctx) {
|
|
46
47
|
const trustLevel = ctx?.context?.friend?.trustLevel;
|
|
47
48
|
const allowed = trustLevel === undefined || (0, types_1.isTrustedLevel)(trustLevel);
|
|
@@ -91,6 +92,83 @@ function parseMailList(value) {
|
|
|
91
92
|
.map((entry) => entry.trim())
|
|
92
93
|
.filter(Boolean);
|
|
93
94
|
}
|
|
95
|
+
function missingPrivateMailKeyId(error) {
|
|
96
|
+
const match = /^(?:Error: )?Missing private mail key ([^\s]+)$/.exec(String(error));
|
|
97
|
+
return match?.[1] ?? null;
|
|
98
|
+
}
|
|
99
|
+
function decryptVisibleMessages(messages, privateKeys) {
|
|
100
|
+
const decrypted = [];
|
|
101
|
+
const skipped = [];
|
|
102
|
+
for (const message of messages) {
|
|
103
|
+
try {
|
|
104
|
+
decrypted.push((0, file_store_1.decryptMessages)([message], privateKeys)[0]);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const keyId = missingPrivateMailKeyId(error);
|
|
108
|
+
if (!keyId)
|
|
109
|
+
throw error;
|
|
110
|
+
skipped.push({ messageId: message.id, keyId });
|
|
111
|
+
(0, runtime_1.emitNervesEvent)({
|
|
112
|
+
component: "repertoire",
|
|
113
|
+
event: "repertoire.mail_decrypt_skipped",
|
|
114
|
+
message: "mail message skipped because its private key is missing",
|
|
115
|
+
meta: { messageId: message.id, keyId },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { decrypted, skipped };
|
|
120
|
+
}
|
|
121
|
+
function renderDecryptSkips(skipped) {
|
|
122
|
+
if (skipped.length === 0)
|
|
123
|
+
return "";
|
|
124
|
+
const noun = skipped.length === 1 ? "message" : "messages";
|
|
125
|
+
const sample = skipped.slice(0, 3).map((entry) => `${entry.messageId} (${entry.keyId})`).join(", ");
|
|
126
|
+
const more = skipped.length > 3 ? `; ${skipped.length - 3} more` : "";
|
|
127
|
+
return [
|
|
128
|
+
`${skipped.length} mail ${noun} could not be decrypted because this agent's vault is missing private mail key material.`,
|
|
129
|
+
`skipped: ${sample}${more}`,
|
|
130
|
+
"recovery: restore the missing private key if available; hosted key rotation can repair future mail, but rotation cannot recover mail already encrypted to a lost private key.",
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
function appendDecryptSkips(body, skipped) {
|
|
134
|
+
const warning = renderDecryptSkips(skipped);
|
|
135
|
+
return warning ? `${body}\n\n${warning}` : body;
|
|
136
|
+
}
|
|
137
|
+
function isRecord(value) {
|
|
138
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
139
|
+
}
|
|
140
|
+
function vaultItemSecretField(rawSecret, item, field) {
|
|
141
|
+
let payload;
|
|
142
|
+
try {
|
|
143
|
+
payload = JSON.parse(rawSecret);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new Error(`vault item ${item} secret payload must be valid JSON`);
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(payload))
|
|
149
|
+
throw new Error(`vault item ${item} secret payload must be an object`);
|
|
150
|
+
const secretFields = isRecord(payload.secretFields) ? payload.secretFields : {};
|
|
151
|
+
const value = [secretFields[field], payload[field]]
|
|
152
|
+
.find((candidate) => typeof candidate === "string" && candidate.trim().length > 0);
|
|
153
|
+
if (!value)
|
|
154
|
+
throw new Error(`vault item ${item} is missing required secret field ${field}`);
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
async function outboundProviderClientForTransport(agentName, transport) {
|
|
158
|
+
return (0, outbound_1.resolveOutboundProviderClient)(transport, {
|
|
159
|
+
readSecretField: async (item, field) => {
|
|
160
|
+
const rawSecret = await (0, credential_access_1.getCredentialStore)(agentName).getRawSecret(item, "password");
|
|
161
|
+
return vaultItemSecretField(rawSecret, item, field);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function renderUndecryptableThread(message, keyId) {
|
|
166
|
+
return [
|
|
167
|
+
`Mail message ${message.id} could not be decrypted because this agent's vault is missing private mail key ${keyId}.`,
|
|
168
|
+
"No body or subject was decrypted.",
|
|
169
|
+
"recovery: restore the missing private key if available; hosted key rotation can repair future mail, but rotation cannot recover mail already encrypted to a lost private key.",
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
94
172
|
function renderMessageSummary(message) {
|
|
95
173
|
const scope = message.compartmentKind === "delegated"
|
|
96
174
|
? `delegated:${message.ownerEmail ?? "unknown"}:${message.source ?? "source"}`
|
|
@@ -250,9 +328,16 @@ function policyScopeForMessage(message) {
|
|
|
250
328
|
return message.source ? `source:${message.source.toLowerCase()}` : message.compartmentKind;
|
|
251
329
|
}
|
|
252
330
|
function normalizePolicySender(candidate, message, privateKeys) {
|
|
331
|
+
let decryptedFrom = [];
|
|
332
|
+
try {
|
|
333
|
+
decryptedFrom = (0, file_store_1.decryptMessages)([message], privateKeys)[0].private.from;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
decryptedFrom = [];
|
|
337
|
+
}
|
|
253
338
|
const candidates = [
|
|
254
339
|
candidate?.senderEmail,
|
|
255
|
-
...
|
|
340
|
+
...decryptedFrom,
|
|
256
341
|
message.envelope.mailFrom,
|
|
257
342
|
].filter((value) => typeof value === "string" && value.trim().length > 0 && value !== "(unknown)");
|
|
258
343
|
for (const candidateValue of candidates) {
|
|
@@ -383,7 +468,11 @@ exports.mailToolDefinitions = [
|
|
|
383
468
|
...(args.source ? { source: args.source } : {}),
|
|
384
469
|
});
|
|
385
470
|
}
|
|
386
|
-
|
|
471
|
+
const result = decryptVisibleMessages(messages, resolved.config.privateKeys);
|
|
472
|
+
if (result.decrypted.length === 0) {
|
|
473
|
+
return appendDecryptSkips("No decryptable mail to show.", result.skipped);
|
|
474
|
+
}
|
|
475
|
+
return appendDecryptSkips(result.decrypted.map(renderMessageSummary).join("\n\n"), result.skipped);
|
|
387
476
|
},
|
|
388
477
|
summaryKeys: ["scope", "placement", "source", "limit"],
|
|
389
478
|
},
|
|
@@ -476,14 +565,16 @@ exports.mailToolDefinitions = [
|
|
|
476
565
|
if (!resolved.ok)
|
|
477
566
|
return resolved.error;
|
|
478
567
|
try {
|
|
568
|
+
const transport = (0, outbound_1.resolveOutboundTransport)(resolved.config);
|
|
479
569
|
const sent = await (0, outbound_1.confirmMailDraftSend)({
|
|
480
570
|
store: resolved.store,
|
|
481
571
|
agentId: resolved.agentName,
|
|
482
572
|
draftId,
|
|
483
|
-
transport
|
|
573
|
+
transport,
|
|
484
574
|
confirmation: args.confirmation ?? "",
|
|
485
575
|
autonomous: args.autonomous === "true",
|
|
486
576
|
autonomyPolicy: resolved.config.autonomousSendPolicy,
|
|
577
|
+
providerClient: await outboundProviderClientForTransport(resolved.agentName, transport),
|
|
487
578
|
actor: actorFromContext(ctx, resolved.agentName),
|
|
488
579
|
reason: args.reason ?? "confirmed outbound send",
|
|
489
580
|
});
|
|
@@ -559,7 +650,8 @@ exports.mailToolDefinitions = [
|
|
|
559
650
|
source: args.source,
|
|
560
651
|
limit: 200,
|
|
561
652
|
});
|
|
562
|
-
const
|
|
653
|
+
const result = decryptVisibleMessages(all, resolved.config.privateKeys);
|
|
654
|
+
const matching = result.decrypted
|
|
563
655
|
.filter((message) => [
|
|
564
656
|
message.private.subject,
|
|
565
657
|
message.private.snippet,
|
|
@@ -582,8 +674,8 @@ exports.mailToolDefinitions = [
|
|
|
582
674
|
});
|
|
583
675
|
}
|
|
584
676
|
if (matching.length === 0)
|
|
585
|
-
return "No matching mail.";
|
|
586
|
-
return matching.map(renderMessageSummary).join("\n\n");
|
|
677
|
+
return appendDecryptSkips("No matching mail.", result.skipped);
|
|
678
|
+
return appendDecryptSkips(matching.map(renderMessageSummary).join("\n\n"), result.skipped);
|
|
587
679
|
},
|
|
588
680
|
summaryKeys: ["query", "limit"],
|
|
589
681
|
},
|
|
@@ -621,7 +713,6 @@ exports.mailToolDefinitions = [
|
|
|
621
713
|
if (blocked)
|
|
622
714
|
return blocked;
|
|
623
715
|
}
|
|
624
|
-
const decrypted = (0, file_store_1.decryptMessages)([message], resolved.config.privateKeys)[0];
|
|
625
716
|
await resolved.store.recordAccess({
|
|
626
717
|
agentId: resolved.agentName,
|
|
627
718
|
messageId,
|
|
@@ -629,6 +720,16 @@ exports.mailToolDefinitions = [
|
|
|
629
720
|
reason: args.reason,
|
|
630
721
|
...accessProvenance(message),
|
|
631
722
|
});
|
|
723
|
+
let decrypted;
|
|
724
|
+
try {
|
|
725
|
+
decrypted = (0, file_store_1.decryptMessages)([message], resolved.config.privateKeys)[0];
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
const keyId = missingPrivateMailKeyId(error);
|
|
729
|
+
if (!keyId)
|
|
730
|
+
throw error;
|
|
731
|
+
return renderUndecryptableThread(message, keyId);
|
|
732
|
+
}
|
|
632
733
|
const maxChars = numberArg(args.max_chars, 2000, 200, 6000);
|
|
633
734
|
const body = decrypted.private.text.length > maxChars
|
|
634
735
|
? `${decrypted.private.text.slice(0, maxChars - 3)}...`
|