@ouro.bot/cli 0.1.0-alpha.833 → 0.1.0-alpha.834
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 +9 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +69 -0
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/a2a/client.js +56 -0
- package/dist/a2a/identity.js +34 -0
- package/dist/a2a/inbound-share.js +3 -0
- package/dist/a2a/onboarding.js +1 -6
- package/dist/a2a/sealed-chat.js +112 -0
- package/dist/a2a/server.js +62 -5
- package/dist/heart/daemon/cli-exec.js +27 -0
- package/dist/heart/daemon/cli-parse.js +52 -0
- package/dist/repertoire/tools.js +16 -4
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/changelog.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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.834",
|
|
6
|
+
"changes": [
|
|
7
|
+
"A2A: authenticated agent chat. A verified friend's message (friends message kind, signed and sealed) becomes the agent's turn under the sender's relationship profile, and the reply is sealed back to the sender and stored sealed, so neither direction travels or persists in plaintext over any transport.",
|
|
8
|
+
"Sanctuary: remote turns are relationship-scoped. Any Sanctuary turn from outside the host (Telegram, A2A, future senses) without a known capability profile gets no tools; only the Butler's own inner, CLI, and MCP channels keep the unscoped tool set. Adds a sanctuary-agent-peer profile: the owner's chat tools minus host execution and approval-gated restarts.",
|
|
9
|
+
"CLI: ouro a2a identity mints a file-backed client key (0600, refused if group- or other-readable); ouro a2a message sends a sealed chat to an agent card and prints the verified reply.",
|
|
10
|
+
"A2A: sealed chat is observable (sealed, opened, and refused events), and a reply sealed as any other friends kind is refused before it can reach a store."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
4
13
|
{
|
|
5
14
|
"version": "0.1.0-alpha.833",
|
|
6
15
|
"changes": [
|
|
@@ -130,6 +130,75 @@
|
|
|
130
130
|
"effectScopes": [
|
|
131
131
|
"telegram.owner_event"
|
|
132
132
|
]
|
|
133
|
+
},
|
|
134
|
+
"sanctuary-agent-peer": {
|
|
135
|
+
"version": 1,
|
|
136
|
+
"toolNames": [
|
|
137
|
+
"external_event_disposition",
|
|
138
|
+
"query_active_work",
|
|
139
|
+
"save_friend_note",
|
|
140
|
+
"telegram_contact_manage",
|
|
141
|
+
"query_cares",
|
|
142
|
+
"care_manage",
|
|
143
|
+
"await_condition",
|
|
144
|
+
"resolve_await",
|
|
145
|
+
"cancel_await",
|
|
146
|
+
"list_recent_attachments",
|
|
147
|
+
"materialize_attachment",
|
|
148
|
+
"describe_image",
|
|
149
|
+
"unraid_list_containers",
|
|
150
|
+
"unraid_get_container_logs",
|
|
151
|
+
"unraid_get_storage",
|
|
152
|
+
"sanctuary_get_media_optimization",
|
|
153
|
+
"sanctuary_search_media_catalog",
|
|
154
|
+
"unraid_get_disks",
|
|
155
|
+
"unraid_get_notifications",
|
|
156
|
+
"unraid_get_system",
|
|
157
|
+
"sanctuary_get_install_state",
|
|
158
|
+
"unraid_check_services",
|
|
159
|
+
"sanctuary_get_download_queue",
|
|
160
|
+
"sanctuary_resume_download_queue",
|
|
161
|
+
"steward_policy_manage",
|
|
162
|
+
"send_message",
|
|
163
|
+
"ponder",
|
|
164
|
+
"rest",
|
|
165
|
+
"settle",
|
|
166
|
+
"speak",
|
|
167
|
+
"shell",
|
|
168
|
+
"shell_status",
|
|
169
|
+
"shell_tail",
|
|
170
|
+
"read_file",
|
|
171
|
+
"write_file",
|
|
172
|
+
"edit_file",
|
|
173
|
+
"glob",
|
|
174
|
+
"grep",
|
|
175
|
+
"web_search",
|
|
176
|
+
"search_facts",
|
|
177
|
+
"consult_diary",
|
|
178
|
+
"consult_notes",
|
|
179
|
+
"get_friend_note",
|
|
180
|
+
"session_summary",
|
|
181
|
+
"query_session",
|
|
182
|
+
"set_reasoning_effort",
|
|
183
|
+
"restart_runtime",
|
|
184
|
+
"revive_sense",
|
|
185
|
+
"media_search",
|
|
186
|
+
"media_request",
|
|
187
|
+
"media_request_status",
|
|
188
|
+
"media_diagnose_and_fix",
|
|
189
|
+
"media_chain_health",
|
|
190
|
+
"media_play_or_resolve"
|
|
191
|
+
],
|
|
192
|
+
"contextScopes": [
|
|
193
|
+
"household.status",
|
|
194
|
+
"household.policy",
|
|
195
|
+
"household.private"
|
|
196
|
+
],
|
|
197
|
+
"effectScopes": [
|
|
198
|
+
"telegram.proactive",
|
|
199
|
+
"telegram.request_return",
|
|
200
|
+
"telegram.owner_event"
|
|
201
|
+
]
|
|
133
202
|
}
|
|
134
203
|
}
|
|
135
204
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.834</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
package/dist/a2a/client.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cardDid = cardDid;
|
|
3
4
|
exports.endpointForCard = endpointForCard;
|
|
4
5
|
exports.fetchA2AAgentCard = fetchA2AAgentCard;
|
|
5
6
|
exports.sendA2AMessage = sendA2AMessage;
|
|
6
7
|
exports.postA2AMessageEnvelope = postA2AMessageEnvelope;
|
|
7
8
|
exports.getA2ATask = getA2ATask;
|
|
9
|
+
exports.sendSealedA2AChat = sendSealedA2AChat;
|
|
8
10
|
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
|
|
9
12
|
const runtime_1 = require("../nerves/runtime");
|
|
13
|
+
const sealed_chat_1 = require("./sealed-chat");
|
|
14
|
+
/** The trimmed `did:key` a card serves, or undefined when the card is did-less. */
|
|
15
|
+
function cardDid(card) {
|
|
16
|
+
const did = card.did;
|
|
17
|
+
return typeof did === "string" && did.trim() ? did.trim() : undefined;
|
|
18
|
+
}
|
|
10
19
|
function endpointForCard(card) {
|
|
11
20
|
const jsonRpc = card.supportedInterfaces?.find((entry) => (entry.protocolBinding ?? entry.transport)?.toUpperCase() === "JSONRPC");
|
|
12
21
|
if (jsonRpc?.url)
|
|
@@ -178,3 +187,50 @@ async function getA2ATask(input) {
|
|
|
178
187
|
}
|
|
179
188
|
return rpc.result;
|
|
180
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Talk to an A2A agent as a verified friend: seal `text` to the agent's card DID,
|
|
192
|
+
* signed by `identity`, send it, and open the sealed reply. The card must serve a
|
|
193
|
+
* did:key bound to it (the same check onboarding applies), so the reply can only be
|
|
194
|
+
* accepted from that key. Neither direction ever travels in plaintext.
|
|
195
|
+
*/
|
|
196
|
+
async function sendSealedA2AChat(input) {
|
|
197
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
198
|
+
const sodium = input.sodium ?? await (0, a2a_client_1.ready)();
|
|
199
|
+
const card = await fetchA2AAgentCard(input.cardUrl, fetchImpl);
|
|
200
|
+
const peerDid = cardDid(card);
|
|
201
|
+
if (!peerDid)
|
|
202
|
+
throw new Error(`A2A card ${input.cardUrl} serves no DID; sealed chat needs one`);
|
|
203
|
+
const parsed = (0, a2a_client_1.parseDidKey)(peerDid);
|
|
204
|
+
if (!parsed || !(0, a2a_client_1.verifyCardDidBinding)({ card: { did: peerDid, url: input.cardUrl }, did: peerDid, didDoc: null })) {
|
|
205
|
+
throw new Error(`A2A card↔DID binding failed for ${peerDid}`);
|
|
206
|
+
}
|
|
207
|
+
const endpointUrl = endpointForCard(card);
|
|
208
|
+
const conversationId = input.conversationId ?? (0, node_crypto_1.randomUUID)();
|
|
209
|
+
const sealed = (0, sealed_chat_1.sealChatMessage)({
|
|
210
|
+
sodium,
|
|
211
|
+
from: input.identity,
|
|
212
|
+
recipientDid: peerDid,
|
|
213
|
+
recipientEd25519Pub: parsed.ed25519Pub,
|
|
214
|
+
text: input.text,
|
|
215
|
+
conversationId,
|
|
216
|
+
});
|
|
217
|
+
const request = {
|
|
218
|
+
jsonrpc: "2.0",
|
|
219
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
220
|
+
method: "SendMessage",
|
|
221
|
+
params: { message: { kind: "message", role: "ROLE_USER", messageId: (0, node_crypto_1.randomUUID)(), contextId: conversationId, parts: sealed.parts } },
|
|
222
|
+
};
|
|
223
|
+
(0, runtime_1.emitNervesEvent)({ component: "channels", event: "channel.a2a_chat_send_start", message: "sending sealed A2A chat message", meta: { endpointUrl, peerDid } });
|
|
224
|
+
const rpc = await postJsonRpc(endpointUrl, request, fetchImpl);
|
|
225
|
+
if ("error" in rpc)
|
|
226
|
+
throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
|
|
227
|
+
const task = rpc.result;
|
|
228
|
+
const replyMessage = task?.status?.message;
|
|
229
|
+
if (!replyMessage)
|
|
230
|
+
throw new Error("A2A chat returned no reply message");
|
|
231
|
+
const opened = await (0, sealed_chat_1.openChatMessage)({ sodium, self: input.identity, message: replyMessage, senderDid: peerDid, senderEd25519Pub: parsed.ed25519Pub });
|
|
232
|
+
if (!opened.ok)
|
|
233
|
+
throw new Error(`A2A chat reply rejected: ${opened.reason}`);
|
|
234
|
+
(0, runtime_1.emitNervesEvent)({ component: "channels", event: "channel.a2a_chat_send_end", message: "received sealed A2A chat reply", meta: { endpointUrl, peerDid, taskId: task.id } });
|
|
235
|
+
return { text: opened.text, conversationId, taskId: task.id, peerDid, peerName: card.name };
|
|
236
|
+
}
|
package/dist/a2a/identity.js
CHANGED
|
@@ -36,7 +36,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.readStoredA2ASeed = readStoredA2ASeed;
|
|
37
37
|
exports.loadOrMintA2AIdentity = loadOrMintA2AIdentity;
|
|
38
38
|
exports.loadSelfA2AIdentity = loadSelfA2AIdentity;
|
|
39
|
+
exports.loadOrMintA2AIdentityFile = loadOrMintA2AIdentityFile;
|
|
39
40
|
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
40
43
|
const os = __importStar(require("node:os"));
|
|
41
44
|
const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
|
|
42
45
|
const runtime_1 = require("../nerves/runtime");
|
|
@@ -168,3 +171,34 @@ async function loadSelfA2AIdentity(input) {
|
|
|
168
171
|
},
|
|
169
172
|
});
|
|
170
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* A file-backed A2A identity for a client that is not an ouro agent (for example a
|
|
176
|
+
* coding harness talking to an agent over A2A). The file holds the Ed25519 seed, so
|
|
177
|
+
* it is treated like an SSH private key: created 0600 inside a 0700 directory, and
|
|
178
|
+
* REFUSED if group or other can read it.
|
|
179
|
+
*/
|
|
180
|
+
async function loadOrMintA2AIdentityFile(input) {
|
|
181
|
+
const sodium = input.sodium ?? await (0, a2a_client_1.ready)();
|
|
182
|
+
return fs.existsSync(input.filePath) ? readA2AIdentityFile(sodium, input.filePath) : mintA2AIdentityFile(sodium, input.filePath);
|
|
183
|
+
}
|
|
184
|
+
function readA2AIdentityFile(sodium, filePath) {
|
|
185
|
+
const mode = fs.statSync(filePath).mode & 0o077;
|
|
186
|
+
if (mode !== 0)
|
|
187
|
+
throw new Error(`A2A identity file ${filePath} is readable by group or other; chmod 600 it`);
|
|
188
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
189
|
+
if (typeof parsed.seed !== "string")
|
|
190
|
+
throw new Error(`A2A identity file ${filePath} has no seed`);
|
|
191
|
+
return deriveIdentity(sodium, decodeSeed(parsed.seed));
|
|
192
|
+
}
|
|
193
|
+
function mintA2AIdentityFile(sodium, filePath) {
|
|
194
|
+
const identity = deriveIdentity(sodium, mintSeed());
|
|
195
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
196
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ version: 1, did: identity.did, seed: identity.seed }, null, 2)}\n`, { mode: 0o600 });
|
|
197
|
+
(0, runtime_1.emitNervesEvent)({
|
|
198
|
+
component: "channels",
|
|
199
|
+
event: "channel.a2a_client_identity_minted",
|
|
200
|
+
message: "minted file-backed A2A client identity",
|
|
201
|
+
meta: { did: identity.did },
|
|
202
|
+
});
|
|
203
|
+
return identity;
|
|
204
|
+
}
|
|
@@ -95,6 +95,9 @@ async function receiveInboundShare(message, deps) {
|
|
|
95
95
|
trust,
|
|
96
96
|
friendsKind: result.friendsKind,
|
|
97
97
|
status: result.status,
|
|
98
|
+
...(result.message ? { message: result.message } : {}),
|
|
99
|
+
/* v8 ignore next -- a completed receipt implies a trusted friend record: an unknown DID reads as stranger and is refused @preserve */
|
|
100
|
+
...(record ? { friend: record } : {}),
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
(0, runtime_1.emitNervesEvent)({
|
package/dist/a2a/onboarding.js
CHANGED
|
@@ -52,11 +52,6 @@ function urlAgentId(cardUrl) {
|
|
|
52
52
|
parsed.hash = "";
|
|
53
53
|
return parsed.toString();
|
|
54
54
|
}
|
|
55
|
-
/** The trimmed `did:key` a card serves, or undefined when the card is did-less. */
|
|
56
|
-
function cardDid(card) {
|
|
57
|
-
const did = card.did;
|
|
58
|
-
return typeof did === "string" && did.trim() ? did.trim() : undefined;
|
|
59
|
-
}
|
|
60
55
|
async function onboardA2APeer(options) {
|
|
61
56
|
// HTTP/URL glue stays harness-side: fetch the agent card, resolve its JSON-RPC
|
|
62
57
|
// endpoint, and derive the stable agentId. The record-shaping (mint/update the
|
|
@@ -77,7 +72,7 @@ async function onboardA2APeer(options) {
|
|
|
77
72
|
// an inbound envelope, so onboarding it would silently break the resolve later). A
|
|
78
73
|
// card whose DID is unparseable or fails the binding is REFUSED (no record
|
|
79
74
|
// written) — never TOFU-trust a mis-bound or malformed card.
|
|
80
|
-
const did = cardDid(card);
|
|
75
|
+
const did = (0, client_1.cardDid)(card);
|
|
81
76
|
if (did) {
|
|
82
77
|
const parsed = (0, a2a_client_1.parseDidKey)(did);
|
|
83
78
|
const bound = parsed !== null && (0, a2a_client_1.verifyCardDidBinding)({ card: { did, url: options.cardUrl }, did, didDoc: null });
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMPTY_CHAT_REPLY = void 0;
|
|
4
|
+
exports.sealChatMessage = sealChatMessage;
|
|
5
|
+
exports.openChatMessage = openChatMessage;
|
|
6
|
+
/**
|
|
7
|
+
* Sealed agent-to-agent chat over A2A (friends `message` kind).
|
|
8
|
+
*
|
|
9
|
+
* Both directions use the same primitive: the text is signed by the sender and
|
|
10
|
+
* sealed to the recipient inside ONE friends DataPart, so a message is confidential
|
|
11
|
+
* and tamper-evident over any transport (tailnet, LAN, relay). The network is a
|
|
12
|
+
* pipe, not the lock.
|
|
13
|
+
*
|
|
14
|
+
* - `sealChatMessage` builds the single-DataPart A2A message for a recipient whose
|
|
15
|
+
* Ed25519 key the sender already trusts (a pinned DID, or a did:key card).
|
|
16
|
+
* - `openChatMessage` opens one through the SAME `receiveShare` pipeline every
|
|
17
|
+
* friends kind uses (unseal, sender binding, pinned-key signature, replay guard,
|
|
18
|
+
* trust floor), pinned to the sender the opener expects. There is no second,
|
|
19
|
+
* hand-rolled verification path.
|
|
20
|
+
*/
|
|
21
|
+
const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
|
|
22
|
+
const friends_1 = require("@ouro.bot/friends");
|
|
23
|
+
const runtime_1 = require("../nerves/runtime");
|
|
24
|
+
/** What an agent replies when its turn produced no text; sealed like any reply. */
|
|
25
|
+
exports.EMPTY_CHAT_REPLY = "(no reply)";
|
|
26
|
+
/** Seal `text` from `from` to the recipient as a single friends DataPart. Returns
|
|
27
|
+
* the harness-shaped A2A message; callers add role/messageId/contextId. */
|
|
28
|
+
function sealChatMessage(input) {
|
|
29
|
+
const text = input.text.trim() === "" ? exports.EMPTY_CHAT_REPLY : input.text.slice(0, friends_1.MAX_MESSAGE_TEXT_CHARS);
|
|
30
|
+
const prepared = (0, friends_1.prepareMessage)({
|
|
31
|
+
fromAgentId: input.from.did,
|
|
32
|
+
text,
|
|
33
|
+
...(input.conversationId !== undefined ? { conversationId: input.conversationId } : {}),
|
|
34
|
+
});
|
|
35
|
+
/* v8 ignore next -- the text is normalized above and the sender DID comes from a real identity @preserve */
|
|
36
|
+
if (!prepared.ok)
|
|
37
|
+
throw new Error(`cannot prepare chat message: ${prepared.status}`);
|
|
38
|
+
const sealed = (0, a2a_client_1.sealEnvelope)({
|
|
39
|
+
sodium: input.sodium,
|
|
40
|
+
envelope: prepared.envelope,
|
|
41
|
+
friendsKind: "message",
|
|
42
|
+
fromIdentity: input.from,
|
|
43
|
+
recipientDid: input.recipientDid,
|
|
44
|
+
recipientX25519Pub: (0, a2a_client_1.keyAgreementFromDidKey)({ sodium: input.sodium, ed25519Pub: input.recipientEd25519Pub }),
|
|
45
|
+
});
|
|
46
|
+
const wrapped = (0, a2a_client_1.wrapInDataPart)({ sealedEnvelope: sealed, recipientDid: input.recipientDid });
|
|
47
|
+
(0, runtime_1.emitNervesEvent)({
|
|
48
|
+
component: "channels",
|
|
49
|
+
event: "channel.a2a_chat_sealed",
|
|
50
|
+
message: "sealed an A2A chat message",
|
|
51
|
+
meta: { recipientDid: input.recipientDid, chars: text.length },
|
|
52
|
+
});
|
|
53
|
+
return { parts: wrapped.parts };
|
|
54
|
+
}
|
|
55
|
+
class MemorySeenLedger {
|
|
56
|
+
seen = new Set();
|
|
57
|
+
isSeen(nonce) { return this.seen.has(nonce); }
|
|
58
|
+
markSeen(nonce) { this.seen.add(nonce); }
|
|
59
|
+
}
|
|
60
|
+
/** A `message` receipt imports nothing, so the stores must never be reached. */
|
|
61
|
+
function unreachableStore(name) {
|
|
62
|
+
return new Proxy({}, { get() { throw new Error(`sealed chat must not touch the ${name}`); } });
|
|
63
|
+
}
|
|
64
|
+
/** Open a sealed chat message from an expected sender through `receiveShare`. */
|
|
65
|
+
async function openChatMessage(input) {
|
|
66
|
+
const pinStore = new a2a_client_1.MemoryPinStore();
|
|
67
|
+
(0, a2a_client_1.pinOnFirstContact)({ pinStore, fromAgentId: input.senderDid, did: input.senderDid, ed25519Pub: input.senderEd25519Pub });
|
|
68
|
+
const result = await (0, a2a_client_1.receiveShare)({
|
|
69
|
+
sodium: input.sodium,
|
|
70
|
+
store: unreachableStore("friend store"),
|
|
71
|
+
missionStore: unreachableStore("mission store"),
|
|
72
|
+
pinStore,
|
|
73
|
+
// Only the pre-pinned sender can verify: any other signer resolves to nothing.
|
|
74
|
+
didResolution: {
|
|
75
|
+
async resolveAndPin({ fromAgentId }) {
|
|
76
|
+
const pinned = pinStore.get(fromAgentId);
|
|
77
|
+
return pinned ? { ed25519Pub: pinned.ed25519Pub } : null;
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
seen: new MemorySeenLedger(),
|
|
81
|
+
a2aMessage: input.message,
|
|
82
|
+
recipientDid: input.self.did,
|
|
83
|
+
recipientIdentity: { x25519Priv: input.self.x25519Priv, x25519Pub: input.self.x25519Pub },
|
|
84
|
+
// The opener chose this sender; the signature, not this level, is the gate.
|
|
85
|
+
trustOfSource: "family",
|
|
86
|
+
});
|
|
87
|
+
if (result.state === "rejected") {
|
|
88
|
+
(0, runtime_1.emitNervesEvent)({
|
|
89
|
+
level: "warn",
|
|
90
|
+
component: "channels",
|
|
91
|
+
event: "channel.a2a_chat_open_rejected",
|
|
92
|
+
message: "refused a sealed A2A chat message",
|
|
93
|
+
meta: { senderDid: input.senderDid, reason: result.reason },
|
|
94
|
+
});
|
|
95
|
+
return { ok: false, reason: result.reason };
|
|
96
|
+
}
|
|
97
|
+
/* v8 ignore next -- any other kind reaches a store, and these stores throw: only a message can complete here @preserve */
|
|
98
|
+
if (result.friendsKind !== "message" || !result.message)
|
|
99
|
+
return { ok: false, reason: "not_a_message" };
|
|
100
|
+
(0, runtime_1.emitNervesEvent)({
|
|
101
|
+
component: "channels",
|
|
102
|
+
event: "channel.a2a_chat_opened",
|
|
103
|
+
message: "opened a verified A2A chat message",
|
|
104
|
+
meta: { senderDid: input.senderDid, chars: result.message.text.length },
|
|
105
|
+
});
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
text: result.message.text,
|
|
109
|
+
...(result.message.conversationId !== undefined ? { conversationId: result.message.conversationId } : {}),
|
|
110
|
+
issuedAt: result.message.issuedAt,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/a2a/server.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.a2aChatRelationship = a2aChatRelationship;
|
|
36
37
|
exports.startA2AServer = startA2AServer;
|
|
37
38
|
const http = __importStar(require("node:http"));
|
|
38
39
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -47,6 +48,8 @@ const pin_store_1 = require("./pin-store");
|
|
|
47
48
|
const seen_ledger_1 = require("./seen-ledger");
|
|
48
49
|
const did_resolution_1 = require("./did-resolution");
|
|
49
50
|
const inbound_share_1 = require("./inbound-share");
|
|
51
|
+
const sealed_chat_1 = require("./sealed-chat");
|
|
52
|
+
const relationship_authorization_1 = require("../repertoire/relationship-authorization");
|
|
50
53
|
const mission_result_wire_1 = require("./mission-result-wire");
|
|
51
54
|
const delegation_stores_1 = require("./delegation-stores");
|
|
52
55
|
const task_store_1 = require("./task-store");
|
|
@@ -155,10 +158,24 @@ async function defaultTurnRunner(input) {
|
|
|
155
158
|
externalId: input.peerAgentId,
|
|
156
159
|
displayName: input.peerName,
|
|
157
160
|
},
|
|
161
|
+
...(input.relationshipAuthorization ? { toolContext: { relationshipAuthorization: input.relationshipAuthorization } } : {}),
|
|
158
162
|
});
|
|
159
163
|
return { response: result.response };
|
|
160
164
|
}
|
|
161
165
|
/* v8 ignore stop */
|
|
166
|
+
/** The relationship that scopes a verified friend's A2A chat turn, from the agent's
|
|
167
|
+
* own capability registry. No registry (or an invalid one) means no relationship,
|
|
168
|
+
* which Sanctuary's tool selection turns into "no tools" for a remote turn. */
|
|
169
|
+
function a2aChatRelationship(agentRoot, friend, requestId) {
|
|
170
|
+
let registry;
|
|
171
|
+
try {
|
|
172
|
+
registry = (0, relationship_authorization_1.loadRelationshipCapabilityRegistry)(agentRoot);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return (0, relationship_authorization_1.createRelationshipAuthorizationEvaluator)({ friend, registry, requestId, requestPhase: "inbound" });
|
|
178
|
+
}
|
|
162
179
|
function accessTokenHash(accessToken) {
|
|
163
180
|
return (0, node_crypto_1.createHash)("sha256").update(accessToken).digest("hex");
|
|
164
181
|
}
|
|
@@ -223,9 +240,15 @@ function taskA2AMetadata(task) {
|
|
|
223
240
|
return {};
|
|
224
241
|
return a2a;
|
|
225
242
|
}
|
|
243
|
+
/** A friends sealed DataPart: opaque ciphertext addressed to one DID, so it is safe
|
|
244
|
+
* to expose to the task holder (a sealed chat reply travels this way). */
|
|
245
|
+
function isSealedFriendsPart(part) {
|
|
246
|
+
const data = part.data;
|
|
247
|
+
return part.kind === "data" && !!data && typeof data === "object" && typeof data.sealed === "object" && data.sealed !== null && typeof data.recipientDid === "string";
|
|
248
|
+
}
|
|
226
249
|
function publicMessage(message, style) {
|
|
227
250
|
const parts = message.parts
|
|
228
|
-
.filter((part) => part && typeof part === "object" && typeof part.text === "string")
|
|
251
|
+
.filter((part) => part && typeof part === "object" && (typeof part.text === "string" || isSealedFriendsPart(part)))
|
|
229
252
|
.map((part) => style === "legacy" ? { ...part, kind: part.kind ?? "text" } : part);
|
|
230
253
|
if (style === "latest")
|
|
231
254
|
return { ...message, kind: "message", parts };
|
|
@@ -293,14 +316,15 @@ function taskFor(input) {
|
|
|
293
316
|
const now = new Date().toISOString();
|
|
294
317
|
const history = [...(input.previousTask?.history ?? []), input.inbound];
|
|
295
318
|
const previousA2A = input.previousTask ? taskA2AMetadata(input.previousTask) : {};
|
|
296
|
-
const
|
|
319
|
+
const responseParts = input.responseParts ?? (input.response ? [{ text: input.response }] : undefined);
|
|
320
|
+
const responseMessage = responseParts
|
|
297
321
|
? {
|
|
298
322
|
kind: "message",
|
|
299
323
|
role: "ROLE_AGENT",
|
|
300
324
|
taskId: input.taskId,
|
|
301
325
|
contextId: input.contextId,
|
|
302
326
|
messageId: (0, node_crypto_1.randomUUID)(),
|
|
303
|
-
parts:
|
|
327
|
+
parts: responseParts,
|
|
304
328
|
}
|
|
305
329
|
: undefined;
|
|
306
330
|
if (responseMessage)
|
|
@@ -438,6 +462,7 @@ async function startA2AServer(options) {
|
|
|
438
462
|
let verifiedPeerAgentId;
|
|
439
463
|
let verifiedPeerName;
|
|
440
464
|
let verifiedShareText;
|
|
465
|
+
let verifiedChat;
|
|
441
466
|
if (inbound && inboundShareDeps && messageHasDataPart(inbound)) {
|
|
442
467
|
const bridged = await (0, inbound_share_1.receiveInboundShare)(inbound, inboundShareDeps);
|
|
443
468
|
if (bridged.outcome === "rejected") {
|
|
@@ -451,7 +476,16 @@ async function startA2AServer(options) {
|
|
|
451
476
|
if (bridged.outcome === "completed") {
|
|
452
477
|
verifiedPeerAgentId = bridged.verifiedDid;
|
|
453
478
|
verifiedPeerName = bridged.verifiedDid;
|
|
454
|
-
|
|
479
|
+
if (bridged.message) {
|
|
480
|
+
// Authenticated chat: the signed, sealed text IS the turn.
|
|
481
|
+
verifiedShareText = bridged.message.text;
|
|
482
|
+
/* v8 ignore next -- a completed chat always has a friend record: an unknown DID reads as stranger and the message is refused @preserve */
|
|
483
|
+
verifiedPeerName = bridged.friend?.name ?? bridged.verifiedDid;
|
|
484
|
+
verifiedChat = { did: bridged.verifiedDid, friend: bridged.friend };
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
verifiedShareText = `[a2a] received ${bridged.friendsKind} (${bridged.status}) from ${bridged.verifiedDid}`;
|
|
488
|
+
}
|
|
455
489
|
}
|
|
456
490
|
}
|
|
457
491
|
const text = verifiedShareText ?? textFromMessage(inbound);
|
|
@@ -486,14 +520,37 @@ async function startA2AServer(options) {
|
|
|
486
520
|
const tokenScope = accessTokenScope(accessToken);
|
|
487
521
|
const clientTaskId = continuationTask ? taskA2AMetadata(continuationTask).clientTaskId : inbound.taskId;
|
|
488
522
|
taskStore.put(taskFor({ taskId, accessToken, contextId, inbound, state: "TASK_STATE_WORKING", clientTaskId, previousTask: continuationTask ?? undefined }), tokenScope);
|
|
523
|
+
const relationshipAuthorization = verifiedChat?.friend
|
|
524
|
+
? a2aChatRelationship(agentRoot, verifiedChat.friend, taskId)
|
|
525
|
+
: undefined;
|
|
489
526
|
const turn = await turnRunner({
|
|
490
527
|
agentName: options.agentName,
|
|
491
528
|
peerAgentId,
|
|
492
529
|
peerName,
|
|
493
530
|
sessionKey: contextId,
|
|
494
531
|
message: text,
|
|
532
|
+
...(relationshipAuthorization ? { relationshipAuthorization } : {}),
|
|
495
533
|
});
|
|
496
|
-
|
|
534
|
+
let responseParts;
|
|
535
|
+
if (verifiedChat) {
|
|
536
|
+
// A verified chat's reply is sealed to the sender and stored sealed, so neither
|
|
537
|
+
// the wire nor a later GetTask ever carries the plaintext.
|
|
538
|
+
const pinned = inboundShareDeps.pinStore.get(verifiedChat.did);
|
|
539
|
+
/* v8 ignore next 4 -- receiveShare pins the verified sender before it reports a completed chat; refuse rather than reply in plaintext @preserve */
|
|
540
|
+
if (!pinned) {
|
|
541
|
+
writeJson(res, 200, errorResponse(rpc.id, -32003, "A2A chat reply cannot be sealed to the sender"));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
responseParts = (0, sealed_chat_1.sealChatMessage)({
|
|
545
|
+
sodium: inboundShareDeps.sodium,
|
|
546
|
+
from: options.identity,
|
|
547
|
+
recipientDid: verifiedChat.did,
|
|
548
|
+
recipientEd25519Pub: pinned.ed25519Pub,
|
|
549
|
+
text: turn.response,
|
|
550
|
+
conversationId: contextId,
|
|
551
|
+
}).parts;
|
|
552
|
+
}
|
|
553
|
+
const task = taskFor({ taskId, accessToken, contextId, inbound, state: "TASK_STATE_COMPLETED", response: turn.response, responseParts, clientTaskId, previousTask: continuationTask ?? undefined });
|
|
497
554
|
taskStore.put(task, tokenScope);
|
|
498
555
|
writeJson(res, 200, jsonResponse(rpc.id, publicTask(task, accessToken, responseStyle, true)));
|
|
499
556
|
return;
|
|
@@ -6062,6 +6062,29 @@ async function executeFriendCommand(command, store) {
|
|
|
6062
6062
|
return `identity not linked: ${command.provider}:${command.externalId}`;
|
|
6063
6063
|
return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
|
|
6064
6064
|
}
|
|
6065
|
+
async function executeA2AClientCommand(command, deps) {
|
|
6066
|
+
const { loadOrMintA2AIdentityFile } = await Promise.resolve().then(() => __importStar(require("../../a2a/identity")));
|
|
6067
|
+
/* v8 ignore next -- production CLI keeps the client key under the home dir; tests pass an explicit file @preserve */
|
|
6068
|
+
const identityFile = command.identityFile ?? path.join(os.homedir(), ".ouro-cli", "a2a", "client-identity.json");
|
|
6069
|
+
const identity = await loadOrMintA2AIdentityFile({ filePath: identityFile });
|
|
6070
|
+
if (command.kind === "a2a.identity") {
|
|
6071
|
+
const message = command.json ? JSON.stringify({ did: identity.did, identityFile }) : `did: ${identity.did}\nidentity file: ${identityFile}`;
|
|
6072
|
+
deps.writeStdout(message);
|
|
6073
|
+
return message;
|
|
6074
|
+
}
|
|
6075
|
+
const { sendSealedA2AChat } = await Promise.resolve().then(() => __importStar(require("../../a2a/client")));
|
|
6076
|
+
const reply = await sendSealedA2AChat({
|
|
6077
|
+
cardUrl: command.to,
|
|
6078
|
+
text: command.text,
|
|
6079
|
+
...(command.conversationId ? { conversationId: command.conversationId } : {}),
|
|
6080
|
+
identity,
|
|
6081
|
+
/* v8 ignore next -- production CLI uses global fetch; tests inject fetch for a hermetic peer @preserve */
|
|
6082
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
6083
|
+
});
|
|
6084
|
+
const message = command.json ? JSON.stringify(reply) : `${reply.peerName}: ${reply.text}\n[conversation: ${reply.conversationId}]`;
|
|
6085
|
+
deps.writeStdout(message);
|
|
6086
|
+
return message;
|
|
6087
|
+
}
|
|
6065
6088
|
async function executeA2ACommand(command, deps) {
|
|
6066
6089
|
if (command.kind === "a2a.card") {
|
|
6067
6090
|
const { buildA2AAgentCard } = await Promise.resolve().then(() => __importStar(require("../../a2a/card")));
|
|
@@ -7132,6 +7155,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7132
7155
|
deps.writeStdout(message);
|
|
7133
7156
|
return message;
|
|
7134
7157
|
}
|
|
7158
|
+
// ── a2a client commands (this machine talks to an agent as a verified friend; no agent, no daemon) ──
|
|
7159
|
+
if (command.kind === "a2a.identity" || command.kind === "a2a.message") {
|
|
7160
|
+
return executeA2AClientCommand(command, deps);
|
|
7161
|
+
}
|
|
7135
7162
|
// ── versions command (local install list + published update truth, no daemon socket needed) ──
|
|
7136
7163
|
if (command.kind === "versions") {
|
|
7137
7164
|
const versions = deps.listCliVersions?.() ?? [];
|
|
@@ -152,6 +152,8 @@ function usage() {
|
|
|
152
152
|
" ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
|
|
153
153
|
" ouro a2a onboard [--agent <name>] --card-url <url> [--trust <level>] [--name <name>]",
|
|
154
154
|
" ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
|
|
155
|
+
" ouro a2a identity [--identity-file <path>] [--json]",
|
|
156
|
+
" ouro a2a message --to <card-url> --text <text> [--context <id>] [--identity-file <path>] [--json]",
|
|
155
157
|
" ouro whoami [--agent <name>]",
|
|
156
158
|
" ouro session list [--agent <name>]",
|
|
157
159
|
" ouro mcp list",
|
|
@@ -1450,6 +1452,56 @@ function parseA2ACommand(args) {
|
|
|
1450
1452
|
}
|
|
1451
1453
|
return { kind: "a2a.card", ...(agent ? { agent } : {}), ...(baseUrl ? { baseUrl } : {}), ...(json ? { json: true } : {}) };
|
|
1452
1454
|
}
|
|
1455
|
+
if (sub === "identity") {
|
|
1456
|
+
let identityFile;
|
|
1457
|
+
let json = false;
|
|
1458
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1459
|
+
if (rest[i] === "--identity-file" && rest[i + 1]) {
|
|
1460
|
+
identityFile = rest[++i];
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (rest[i] === "--json") {
|
|
1464
|
+
json = true;
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
throw new Error("Usage: ouro a2a identity [--identity-file <path>] [--json]");
|
|
1468
|
+
}
|
|
1469
|
+
return { kind: "a2a.identity", ...(identityFile ? { identityFile } : {}), ...(json ? { json: true } : {}) };
|
|
1470
|
+
}
|
|
1471
|
+
if (sub === "message") {
|
|
1472
|
+
const usageText = "Usage: ouro a2a message --to <card-url> --text <text> [--context <id>] [--identity-file <path>] [--json]";
|
|
1473
|
+
let to;
|
|
1474
|
+
let text;
|
|
1475
|
+
let conversationId;
|
|
1476
|
+
let identityFile;
|
|
1477
|
+
let json = false;
|
|
1478
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1479
|
+
if (rest[i] === "--to" && rest[i + 1]) {
|
|
1480
|
+
to = rest[++i];
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (rest[i] === "--text" && rest[i + 1]) {
|
|
1484
|
+
text = rest[++i];
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (rest[i] === "--context" && rest[i + 1]) {
|
|
1488
|
+
conversationId = rest[++i];
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (rest[i] === "--identity-file" && rest[i + 1]) {
|
|
1492
|
+
identityFile = rest[++i];
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (rest[i] === "--json") {
|
|
1496
|
+
json = true;
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
throw new Error(usageText);
|
|
1500
|
+
}
|
|
1501
|
+
if (!to || !text)
|
|
1502
|
+
throw new Error(usageText);
|
|
1503
|
+
return { kind: "a2a.message", to, text, ...(conversationId ? { conversationId } : {}), ...(identityFile ? { identityFile } : {}), ...(json ? { json: true } : {}) };
|
|
1504
|
+
}
|
|
1453
1505
|
if (sub === "onboard") {
|
|
1454
1506
|
let cardUrl;
|
|
1455
1507
|
let trustLevel;
|
package/dist/repertoire/tools.js
CHANGED
|
@@ -74,6 +74,8 @@ exports.SANCTUARY_MCP_PROVIDED_TOOLS = new Set([
|
|
|
74
74
|
"media_search", "media_request", "media_request_status",
|
|
75
75
|
"media_diagnose_and_fix", "media_chain_health", "media_play_or_resolve",
|
|
76
76
|
]);
|
|
77
|
+
/** Channels a Sanctuary turn can arrive on from the host itself, not from a remote party. */
|
|
78
|
+
const SANCTUARY_LOCAL_CHANNELS = new Set(["inner", "cli", "mcp"]);
|
|
77
79
|
exports.SANCTUARY_OWNER_ADDITIONS = new Set([
|
|
78
80
|
"shell", "shell_status", "shell_tail", "read_file", "write_file", "edit_file", "glob", "grep",
|
|
79
81
|
"web_search", "search_facts", "consult_diary", "consult_notes", "get_friend_note",
|
|
@@ -125,10 +127,20 @@ function applyPreference(tool, pref) {
|
|
|
125
127
|
function selectToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager, _chatModel, context) {
|
|
126
128
|
const relationship = context?.relationshipAuthorization;
|
|
127
129
|
const profileId = relationship?.profileId;
|
|
128
|
-
|
|
130
|
+
// Every Sanctuary turn that reaches the agent from outside the host (Telegram, A2A,
|
|
131
|
+
// any future remote sense) is relationship-scoped: without a known capability
|
|
132
|
+
// profile it gets no tools at all. Only the Butler's own local channels (its inner
|
|
133
|
+
// loop and the host-local CLI/MCP) keep the unscoped tool set.
|
|
134
|
+
const channel = capabilities?.channel;
|
|
135
|
+
const remote = channel !== undefined && !SANCTUARY_LOCAL_CHANNELS.has(channel);
|
|
136
|
+
const sanctuary = (context?.agentName === "sanctuary" && (relationship !== undefined || remote))
|
|
129
137
|
|| profileId?.startsWith("sanctuary-") === true;
|
|
130
138
|
const owner = profileId === "sanctuary-owner";
|
|
131
|
-
|
|
139
|
+
// A trusted peer agent acting for the owner gets the owner's chat tool menu. Host
|
|
140
|
+
// execution and approval-gated routine actions stay Telegram-owner-only: the host
|
|
141
|
+
// tool is only ever added on the Telegram channel, and the peer profile omits them.
|
|
142
|
+
const ownerLike = owner || profileId === "sanctuary-agent-peer";
|
|
143
|
+
const knownSanctuaryProfile = relationship !== undefined && (ownerLike || profileId === "sanctuary-household" || profileId === "sanctuary-event");
|
|
132
144
|
const mcp = mcpManager ? (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpManager) : [];
|
|
133
145
|
const native = [...tools_base_1.baseToolDefinitions, ...additionalDefinitions];
|
|
134
146
|
assertUniqueToolSchemas([
|
|
@@ -140,8 +152,8 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
|
|
|
140
152
|
ordinary = knownSanctuaryProfile
|
|
141
153
|
? [
|
|
142
154
|
...tools_base_1.baseToolDefinitions.filter((definition) => SANCTUARY_RELATIONSHIP_BASE_TOOLS.has(definition.tool.function.name)
|
|
143
|
-
|| (
|
|
144
|
-
...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...(
|
|
155
|
+
|| (ownerLike && exports.SANCTUARY_OWNER_ADDITIONS.has(definition.tool.function.name))),
|
|
156
|
+
...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...(ownerLike ? mcp : []),
|
|
145
157
|
]
|
|
146
158
|
: [];
|
|
147
159
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.834",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.834",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@microsoft/teams.common": "2.0.11",
|
|
19
19
|
"@microsoft/teams.dev": "2.0.11",
|
|
20
20
|
"@microsoft/teams.graph": "2.0.11",
|
|
21
|
-
"@ouro.bot/friends": "0.1.0-alpha.
|
|
21
|
+
"@ouro.bot/friends": "0.1.0-alpha.12",
|
|
22
22
|
"@types/react": "^17.0.91",
|
|
23
23
|
"@types/ws": "^8.18.1",
|
|
24
24
|
"ajv": "^8.20.0",
|
|
@@ -1357,9 +1357,9 @@
|
|
|
1357
1357
|
}
|
|
1358
1358
|
},
|
|
1359
1359
|
"node_modules/@ouro.bot/friends": {
|
|
1360
|
-
"version": "0.1.0-alpha.
|
|
1361
|
-
"resolved": "https://registry.npmjs.org/@ouro.bot/friends/-/friends-0.1.0-alpha.
|
|
1362
|
-
"integrity": "sha512-
|
|
1360
|
+
"version": "0.1.0-alpha.12",
|
|
1361
|
+
"resolved": "https://registry.npmjs.org/@ouro.bot/friends/-/friends-0.1.0-alpha.12.tgz",
|
|
1362
|
+
"integrity": "sha512-pzV2PQvi1eUkcog3iDDHIvwSBDwpsB8/ykrJpoZQ/wB84qQa0xGKJSDnTt45r8H4j/68Cc5LU6vcjlwI5GG47g==",
|
|
1363
1363
|
"license": "Apache-2.0",
|
|
1364
1364
|
"dependencies": {
|
|
1365
1365
|
"libsodium-wrappers": "0.8.4"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.834",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@microsoft/teams.common": "2.0.11",
|
|
66
66
|
"@microsoft/teams.dev": "2.0.11",
|
|
67
67
|
"@microsoft/teams.graph": "2.0.11",
|
|
68
|
-
"@ouro.bot/friends": "0.1.0-alpha.
|
|
68
|
+
"@ouro.bot/friends": "0.1.0-alpha.12",
|
|
69
69
|
"@types/react": "^17.0.91",
|
|
70
70
|
"@types/ws": "^8.18.1",
|
|
71
71
|
"ajv": "^8.20.0",
|