@ouro.bot/cli 0.1.0-alpha.876 → 0.1.0-alpha.878

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,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ESCALATION_CONFIRM_INTERVAL_MS = void 0;
36
37
  exports.a2aChatRelationship = a2aChatRelationship;
37
38
  exports.startA2AServer = startA2AServer;
38
39
  const http = __importStar(require("node:http"));
@@ -55,8 +56,11 @@ const mission_result_wire_1 = require("./mission-result-wire");
55
56
  const delegation_stores_1 = require("./delegation-stores");
56
57
  const task_store_1 = require("./task-store");
57
58
  const delegated_command_1 = require("./delegated-command");
59
+ const outbox_wire_1 = require("./outbox-wire");
60
+ const failure_reports_1 = require("../heart/failure-reports");
58
61
  const MAX_A2A_REQUEST_BYTES = 128 * 1024;
59
62
  const MAX_A2A_MESSAGE_TEXT_CHARS = 16_000;
63
+ exports.ESCALATION_CONFIRM_INTERVAL_MS = 5 * 60 * 1000;
60
64
  class A2ARequestError extends Error {
61
65
  status;
62
66
  code;
@@ -394,6 +398,19 @@ async function startA2AServer(options) {
394
398
  identity: options.identity,
395
399
  };
396
400
  }
401
+ // Resolved failure reports are confirmed to the owner once the running version reaches the fix: now, after each
402
+ // resolution, and on a timer, so a fix that lands with a restart is announced without anyone calling again.
403
+ const confirmReports = async () => {
404
+ if (!options.escalation)
405
+ return;
406
+ try {
407
+ await (0, failure_reports_1.confirmResolvedReports)(agentRoot, { ...options.escalation, ...(inboundShareDeps ? { friends: inboundShareDeps.store } : {}) });
408
+ }
409
+ catch (error) {
410
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "channels", event: "channel.a2a_escalation_confirm_error", message: "confirming resolved failure reports failed", meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) } });
411
+ }
412
+ };
413
+ let confirmTimer;
397
414
  let publicBaseUrl = options.baseUrl;
398
415
  const server = http.createServer(async (req, res) => {
399
416
  /* v8 ignore next -- Node always supplies url/host for accepted HTTP requests; fallback keeps malformed local calls safe @preserve */
@@ -427,6 +444,48 @@ async function startA2AServer(options) {
427
444
  return;
428
445
  }
429
446
  try {
447
+ if ((0, outbox_wire_1.isOutboxMethod)(rpc.method)) {
448
+ // Peer outbox methods: signed and sealed like a chat, access decided here from the verified friend record.
449
+ const outboxInbound = messageFromParams(rpc.params);
450
+ if (!outboxInbound || !inboundShareDeps) {
451
+ writeJson(res, 200, errorResponse(rpc.id, outbox_wire_1.OUTBOX_ERROR_REFUSED, "outbox calls need a signed message and an A2A identity"));
452
+ return;
453
+ }
454
+ const bridged = await (0, inbound_share_1.receiveInboundShare)({ ...outboxInbound, kind: outboxInbound.kind ?? "message" }, inboundShareDeps);
455
+ if (bridged.outcome === "rejected") {
456
+ writeJson(res, 200, errorResponse(rpc.id, rejectionErrorCode(bridged.reason), `A2A outbox call rejected: ${bridged.reason}`));
457
+ return;
458
+ }
459
+ if (bridged.outcome !== "completed" || !bridged.message) {
460
+ writeJson(res, 200, errorResponse(rpc.id, outbox_wire_1.OUTBOX_ERROR_REFUSED, "outbox calls must be a signed chat message"));
461
+ return;
462
+ }
463
+ const command = (0, outbox_wire_1.parseOutboxCommand)(bridged.message.text);
464
+ if (!command || command.method !== rpc.method) {
465
+ writeJson(res, 200, errorResponse(rpc.id, outbox_wire_1.OUTBOX_ERROR_INVALID, "the signed method does not match the requested method"));
466
+ return;
467
+ }
468
+ const outcome = await (0, outbox_wire_1.handleOutboxCommand)({ agentRoot, friend: bridged.friend, method: command.method, params: command.params, verifiedDid: bridged.verifiedDid });
469
+ if (!outcome.ok) {
470
+ writeJson(res, 200, errorResponse(rpc.id, outcome.code, outcome.message));
471
+ return;
472
+ }
473
+ if (command.method === "report/resolve")
474
+ await confirmReports();
475
+ /* v8 ignore next -- receiveShare pins the verified sender before it reports a completed message @preserve */
476
+ const outboxPinned = inboundShareDeps.pinStore.get(bridged.verifiedDid);
477
+ const contextId = outboxInbound.contextId ?? "default";
478
+ const reply = (0, sealed_chat_1.sealChatMessage)({
479
+ sodium: inboundShareDeps.sodium,
480
+ from: options.identity,
481
+ recipientDid: bridged.verifiedDid,
482
+ recipientEd25519Pub: outboxPinned.ed25519Pub,
483
+ text: JSON.stringify(outcome.result),
484
+ conversationId: contextId,
485
+ });
486
+ writeJson(res, 200, jsonResponse(rpc.id, { kind: "message", role: "ROLE_AGENT", messageId: (0, node_crypto_1.randomUUID)(), contextId, parts: reply.parts }));
487
+ return;
488
+ }
430
489
  if (rpc.method === "SendMessage" || rpc.method === "message/send") {
431
490
  const responseStyle = rpc.method === "message/send" ? "legacy" : "latest";
432
491
  const parsedInbound = messageFromParams(rpc.params);
@@ -671,6 +730,11 @@ async function startA2AServer(options) {
671
730
  endpointUrl.pathname = a2aPath;
672
731
  endpointUrl.search = "";
673
732
  endpointUrl.hash = "";
733
+ if (options.escalation) {
734
+ await confirmReports();
735
+ confirmTimer = setInterval(() => { void confirmReports(); }, options.escalation.confirmIntervalMs ?? exports.ESCALATION_CONFIRM_INTERVAL_MS);
736
+ confirmTimer.unref();
737
+ }
674
738
  (0, runtime_1.emitNervesEvent)({
675
739
  component: "channels",
676
740
  event: "channel.a2a_server_started",
@@ -682,6 +746,8 @@ async function startA2AServer(options) {
682
746
  url: publicBaseUrl,
683
747
  endpointUrl: endpointUrl.toString(),
684
748
  close: () => new Promise((resolve, reject) => {
749
+ if (confirmTimer)
750
+ clearInterval(confirmTimer);
685
751
  server.close((error) => {
686
752
  if (error) {
687
753
  reject(error);
@@ -0,0 +1,98 @@
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.TRUSTED_UID = void 0;
37
+ exports.overrideTrustedUidForTests = overrideTrustedUidForTests;
38
+ exports.isTrustedDirectory = isTrustedDirectory;
39
+ exports.readTrustedJson = readTrustedJson;
40
+ const fs = __importStar(require("node:fs"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ /**
43
+ * Operator-set state lives in the agent bundle, which the agent's own process (and anything a prompt-injected model can
44
+ * run through its shell) can write. Such state only counts when root owns it and nobody else can write it, the same
45
+ * rule the replay window follows. Anything else reads as "not there", so every consumer fails closed.
46
+ */
47
+ exports.TRUSTED_UID = 0;
48
+ let uidOverride;
49
+ /** Only a test runner sets VITEST; the running Butler (and its prompt-injected shell) cannot reach this process's environment. */
50
+ const underTestRunner = () => process.env.VITEST !== undefined;
51
+ /** Tests run as an ordinary user, so they declare that user trusted. Refused outside a test runner, and ignored there too. */
52
+ function overrideTrustedUidForTests(uid) {
53
+ if (!underTestRunner())
54
+ throw new Error("overrideTrustedUidForTests is for tests only");
55
+ uidOverride = uid;
56
+ }
57
+ const effectiveTrustedUid = () => (underTestRunner() ? uidOverride : undefined) ?? exports.TRUSTED_UID;
58
+ /** A real directory (no symlink), owned by the trusted uid, writable by neither group nor other. */
59
+ function isTrustedDirectory(target, trustedUid = effectiveTrustedUid()) {
60
+ let stat;
61
+ try {
62
+ stat = fs.lstatSync(target);
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ const trusted = stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === trustedUid && (stat.mode & 0o022) === 0;
68
+ if (!trusted)
69
+ rejected(target, "directory is not a real directory owned by the trusted uid and closed to group and other writes");
70
+ return trusted;
71
+ }
72
+ function rejected(target, reason) {
73
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_trusted_file_rejected", message: "ignored operator-set state the agent could have written", meta: { target, reason } });
74
+ }
75
+ /**
76
+ * Parses a JSON file only if the file actually opened is trusted. The path is opened once with O_NOFOLLOW, then the
77
+ * descriptor itself is checked (regular file, trusted owner, not group- or other-writable) and read, so a rename loop
78
+ * cannot swap a different file in between the check and the read. Any error is `undefined`.
79
+ */
80
+ function readTrustedJson(file, trustedUid = effectiveTrustedUid()) {
81
+ let fd;
82
+ try {
83
+ fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
84
+ const stat = fs.fstatSync(fd);
85
+ if (!stat.isFile() || stat.uid !== trustedUid || (stat.mode & 0o022) !== 0) {
86
+ rejected(file, "file is not a regular file owned by the trusted uid and closed to group and other writes");
87
+ return undefined;
88
+ }
89
+ return JSON.parse(fs.readFileSync(fd, "utf8"));
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ finally {
95
+ if (fd !== undefined)
96
+ fs.closeSync(fd);
97
+ }
98
+ }
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PEER_ASKED_OWNER_TEXT = void 0;
36
37
  exports.askedOwnerText = askedOwnerText;
37
38
  exports.createA2AAwaitOwnerDeliverer = createA2AAwaitOwnerDeliverer;
38
39
  exports.defaultNotifyOwner = defaultNotifyOwner;
@@ -40,14 +41,22 @@ const path = __importStar(require("path"));
40
41
  const friends_1 = require("@ouro.bot/friends");
41
42
  const identity_1 = require("../identity");
42
43
  const replay_harness_1 = require("../../a2a/replay-harness");
44
+ const outbox_store_1 = require("../../a2a/outbox-store");
43
45
  const runtime_1 = require("../../nerves/runtime");
44
46
  /**
45
47
  * An A2A peer is a client that calls this agent's server; there is no channel back to it. When an await filed from
46
48
  * an A2A conversation resolves or expires, the outcome goes to the agent's owner in the owner's own Telegram chat,
47
49
  * written by the agent as itself (an owner notice, the same path that announces delegated commands). Nothing is
48
50
  * sent as the owner or to the peer, and the notice id is the await's delivery id, so a retry cannot double-send.
51
+ *
52
+ * An await filed in a plain A2A conversation (no request id) was not delegated by the owner, so the owner hears nothing
53
+ * and the outcome goes to the peer instead: into its own outbox, which it reads with `outbox/list`. A question that needs
54
+ * the owner's decision is the exception, because only the owner can answer it: the peer is told, in one fixed
55
+ * sentence that never repeats the question, that the owner was asked, and the owner is asked as before.
49
56
  */
50
57
  const NOTICE_ID_MAX_BYTES = 512;
58
+ /** The owner's question can carry anything the Butler read or wrote, so a plain peer is told only that the owner was asked. */
59
+ exports.PEER_ASKED_OWNER_TEXT = "I've asked my owner and will let you know.";
51
60
  /** A question for the owner reads as the owner's own question, with a pointer to the request it is about; every other outcome is a follow-up report. */
52
61
  function askedOwnerText(question, about) {
53
62
  return `${question}\n\n(about the request from ${about})`;
@@ -64,6 +73,15 @@ function createA2AAwaitOwnerDeliverer(agentName, notifyOwner = defaultNotifyOwne
64
73
  const peer = await new friends_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(agentName), "friends")).get(request.friendId);
65
74
  const agentRoot = (0, identity_1.getAgentRoot)(agentName);
66
75
  const from = peer?.name ?? "a connected agent";
76
+ const peerAwait = !request.requestId;
77
+ if (peerAwait) {
78
+ const body = request.noticeKind === "asked_owner"
79
+ ? exports.PEER_ASKED_OWNER_TEXT
80
+ : request.content;
81
+ await new outbox_store_1.FileOutboxStore(agentRoot).appendOnce(request.friendId, request.deliveryId, { kind: "await_outcome", body, meta: { outcome: request.noticeKind ?? "follow_up" } });
82
+ if (request.noticeKind !== "asked_owner")
83
+ return { status: "delivered_now", detail: "posted to the peer's outbox" };
84
+ }
67
85
  if ((0, replay_harness_1.isReplayWindowOpen)(agentRoot, request.friendId)) {
68
86
  (0, replay_harness_1.appendReplayNotice)(agentRoot, { noticeId: request.deliveryId, text: request.noticeKind === "asked_owner" ? ownerNoticeText(request, from) : request.content, friendId: request.friendId });
69
87
  return { status: "delivered_now", detail: "written to the replay sink (replay window open for this peer)" };
@@ -525,6 +525,7 @@ function agentResolutionFailureMode(command) {
525
525
  case "a2a.card":
526
526
  case "a2a.onboard":
527
527
  case "a2a.serve":
528
+ case "a2a.escalation":
528
529
  return "throw";
529
530
  case "provider.use":
530
531
  case "provider.check":
@@ -6098,6 +6099,26 @@ async function executeA2AClientCommand(command, deps) {
6098
6099
  deps.writeStdout(message);
6099
6100
  return message;
6100
6101
  }
6102
+ if (command.kind === "a2a.outbox") {
6103
+ const { callOutboxMethod } = await Promise.resolve().then(() => __importStar(require("../../a2a/outbox-client")));
6104
+ const call = command.action === "list" ? { method: "outbox/list", params: { ...(command.since ? { since: command.since } : {}) } }
6105
+ : command.action === "ack" ? { method: "outbox/ack", params: { ids: command.ids } }
6106
+ : await (async () => {
6107
+ // The resolution is signed with this machine's key: the Butler tells its owner "fixed" only for a holder's signature.
6108
+ const { signResolution } = await Promise.resolve().then(() => __importStar(require("../../a2a/resolution-proof")));
6109
+ const { ready } = await Promise.resolve().then(() => __importStar(require("@ouro.bot/friends/a2a-client")));
6110
+ const claim = { reportId: command.reportId, version: command.version, note: command.note, resolvedAt: new Date().toISOString() };
6111
+ return { method: "report/resolve", params: { id: claim.reportId, version: claim.version, note: claim.note, resolvedAt: claim.resolvedAt, proof: signResolution({ sodium: await ready(), identity, claim }) } };
6112
+ })();
6113
+ const result = await callOutboxMethod({
6114
+ cardUrl: command.to, ...call, identity,
6115
+ /* v8 ignore next -- production CLI uses global fetch; tests inject fetch for a hermetic peer @preserve */
6116
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
6117
+ });
6118
+ const message = command.json ? JSON.stringify(result) : JSON.stringify(result, null, 2);
6119
+ deps.writeStdout(message);
6120
+ return message;
6121
+ }
6101
6122
  const { sendSealedA2AChat } = await Promise.resolve().then(() => __importStar(require("../../a2a/client")));
6102
6123
  const reply = await sendSealedA2AChat({
6103
6124
  cardUrl: command.to,
@@ -6132,6 +6153,37 @@ async function executeA2ACommand(command, deps) {
6132
6153
  deps.writeStdout(message);
6133
6154
  return message;
6134
6155
  }
6156
+ if (command.kind === "a2a.escalation") {
6157
+ const { readEscalationGrants, setEscalationGrant } = await Promise.resolve().then(() => __importStar(require("../../a2a/escalation-grants")));
6158
+ /* v8 ignore next -- production default bundles root; tests inject bundlesRoot @preserve */
6159
+ const agentRoot = path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
6160
+ if (command.action === "list") {
6161
+ const grants = Object.entries(readEscalationGrants(agentRoot));
6162
+ const message = grants.length === 0 ? "no escalation grants" : grants.map(([id, grant]) => `${id} granted ${grant.grantedAt} ${grant.did} ${grant.source}`).join("\n");
6163
+ deps.writeStdout(message);
6164
+ return message;
6165
+ }
6166
+ const friend = await new friends_1.FileFriendStore(path.join(agentRoot, "friends")).get(command.friendId);
6167
+ if (!friend)
6168
+ throw new Error(`friend not found: ${command.friendId}`);
6169
+ const at = new Date(deps.now?.() ?? Date.now());
6170
+ const { friendDid } = await Promise.resolve().then(() => __importStar(require("../../a2a/resolution-proof")));
6171
+ const holderDid = command.action === "grant" ? friendDid(friend) : null;
6172
+ if (command.action === "grant" && !holderDid)
6173
+ throw new Error(`${friend.name} has no pinned DID yet; onboard it with a DID before granting escalation`);
6174
+ const change = command.action === "grant"
6175
+ ? setEscalationGrant(agentRoot, friend.id, { grant: true, source: command.source ?? `ouro a2a escalation grant, ${at.toISOString()}`, did: holderDid }, at)
6176
+ : setEscalationGrant(agentRoot, friend.id, { grant: false }, at);
6177
+ const message = [
6178
+ `${command.action === "grant" ? "granted" : "revoked"} escalation: ${friend.name} (${friend.id})${change.changed ? "" : " (no change)"}`,
6179
+ ...(holderDid ? [`pinned DID: ${holderDid} (compare it with the holder's real DID)`] : []),
6180
+ ...(change.backup ? [`backup: ${change.backup}`] : []),
6181
+ ...(command.action === "grant" && readEscalationGrants(agentRoot)[friend.id] === undefined ? [`WARNING: the grant is written but will not be honoured: ${path.join(agentRoot, "state", "a2a")} and escalation-grants.json must be owned by root and writable by no one else. Run this command as root (docker exec as root) or chown root:root and chmod 755/644 them.`] : []),
6182
+ ...(command.action === "grant" && (friend.trustLevel !== "family" || friend.admissionState !== "active") ? [`note: ${friend.name} only holds the grant while it is active family (now ${friend.trustLevel}, ${friend.admissionState})`] : []),
6183
+ ].join("\n");
6184
+ deps.writeStdout(message);
6185
+ return message;
6186
+ }
6135
6187
  /* v8 ignore next -- false branch is foreground serve, intentionally ignored below because it waits for signals @preserve */
6136
6188
  if (command.kind === "a2a.onboard") {
6137
6189
  if (command.did) {
@@ -6179,8 +6231,10 @@ async function executeA2ACommand(command, deps) {
6179
6231
  }
6180
6232
  /* v8 ignore start -- foreground serve intentionally waits for process signals; a2a/server has route-level coverage @preserve */
6181
6233
  const { startA2AServer } = await Promise.resolve().then(() => __importStar(require("../../a2a/server")));
6234
+ const { escalationOptionsFor, sendOwnerNoticeViaTelegram } = await Promise.resolve().then(() => __importStar(require("../../a2a/escalation-wiring")));
6182
6235
  const handle = await startA2AServer({
6183
6236
  agentName: command.agent,
6237
+ ...escalationOptionsFor(command.agent, (notice) => sendOwnerNoticeViaTelegram(command.agent, notice)),
6184
6238
  ...(command.host ? { host: command.host } : {}),
6185
6239
  ...(command.port ? { port: command.port } : {}),
6186
6240
  ...(command.baseUrl ? { baseUrl: command.baseUrl } : {}),
@@ -7204,7 +7258,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7204
7258
  return message;
7205
7259
  }
7206
7260
  // ── a2a client commands (this machine talks to an agent as a verified friend; no agent, no daemon) ──
7207
- if (command.kind === "a2a.identity" || command.kind === "a2a.message") {
7261
+ if (command.kind === "a2a.identity" || command.kind === "a2a.message" || command.kind === "a2a.outbox") {
7208
7262
  return executeA2AClientCommand(command, deps);
7209
7263
  }
7210
7264
  // ── versions command (local install list + published update truth, no daemon socket needed) ──
@@ -7908,7 +7962,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7908
7962
  deps.writeStdout(message);
7909
7963
  return message;
7910
7964
  }
7911
- if (command.kind === "a2a.card" || command.kind === "a2a.onboard" || command.kind === "a2a.serve") {
7965
+ if (command.kind === "a2a.card" || command.kind === "a2a.onboard" || command.kind === "a2a.serve" || command.kind === "a2a.escalation") {
7912
7966
  return executeA2ACommand(command, deps);
7913
7967
  }
7914
7968
  // ── provider commands (local, no daemon socket needed) ──
@@ -154,6 +154,11 @@ function usage() {
154
154
  " ouro a2a serve [--agent <name>] [--host <host>] [--port <port>] [--base-url <url>] [--path <path>]",
155
155
  " ouro a2a identity [--identity-file <path>] [--json]",
156
156
  " ouro a2a message --to <card-url> --text <text> [--context <id>] [--delegated] [--identity-file <path>] [--json]",
157
+ " ouro a2a outbox list --to <card-url> [--since <cursor>] [--identity-file <path>] [--json]",
158
+ " ouro a2a outbox ack --to <card-url> --ids <id,id,...> [--identity-file <path>] [--json]",
159
+ " ouro a2a outbox resolve --to <card-url> --report <id> --version <version> --note <text> [--identity-file <path>] [--json]",
160
+ " ouro a2a escalation <grant|revoke> --friend <id> [--source <text>] [--agent <name>]",
161
+ " ouro a2a escalation list [--agent <name>]",
157
162
  " ouro whoami [--agent <name>]",
158
163
  " ouro session list [--agent <name>]",
159
164
  " ouro mcp list",
@@ -1550,6 +1555,65 @@ function parseA2ACommand(args) {
1550
1555
  throw new Error(usageText);
1551
1556
  return { kind: "a2a.message", to, text, ...(conversationId ? { conversationId } : {}), ...(delegated ? { delegated: true } : {}), ...(identityFile ? { identityFile } : {}), ...(json ? { json: true } : {}) };
1552
1557
  }
1558
+ if (sub === "outbox") {
1559
+ const usageText = "Usage: ouro a2a outbox <list|ack|resolve> --to <card-url> [--since <cursor>] [--ids <id,id,...>] [--report <id> --version <version> --note <text>] [--identity-file <path>] [--json]";
1560
+ const action = rest[0];
1561
+ if (action !== "list" && action !== "ack" && action !== "resolve")
1562
+ throw new Error(usageText);
1563
+ const values = {};
1564
+ let json = false;
1565
+ for (let i = 1; i < rest.length; i += 1) {
1566
+ if (rest[i] === "--json") {
1567
+ json = true;
1568
+ continue;
1569
+ }
1570
+ const flag = rest[i];
1571
+ if (["--to", "--since", "--ids", "--report", "--version", "--note", "--identity-file"].includes(flag) && rest[i + 1]) {
1572
+ values[flag] = rest[++i];
1573
+ continue;
1574
+ }
1575
+ throw new Error(usageText);
1576
+ }
1577
+ const allowed = { list: ["--to", "--since", "--identity-file"], ack: ["--to", "--ids", "--identity-file"], resolve: ["--to", "--report", "--version", "--note", "--identity-file"] };
1578
+ const required = { list: ["--to"], ack: ["--to", "--ids"], resolve: ["--to", "--report", "--version", "--note"] };
1579
+ if (Object.keys(values).some((flag) => !allowed[action].includes(flag)) || required[action].some((flag) => !values[flag]))
1580
+ throw new Error(usageText);
1581
+ const ids = values["--ids"]?.split(",").map((id) => id.trim()).filter(Boolean);
1582
+ if (action === "ack" && (!ids || ids.length === 0))
1583
+ throw new Error(usageText);
1584
+ return {
1585
+ kind: "a2a.outbox", action, to: values["--to"],
1586
+ ...(values["--since"] ? { since: values["--since"] } : {}),
1587
+ ...(action === "ack" ? { ids: ids } : {}),
1588
+ ...(values["--report"] ? { reportId: values["--report"] } : {}),
1589
+ ...(values["--version"] ? { version: values["--version"] } : {}),
1590
+ ...(values["--note"] ? { note: values["--note"] } : {}),
1591
+ ...(values["--identity-file"] ? { identityFile: values["--identity-file"] } : {}),
1592
+ ...(json ? { json: true } : {}),
1593
+ };
1594
+ }
1595
+ if (sub === "escalation") {
1596
+ const usageText = "Usage: ouro a2a escalation <grant|revoke> --friend <id> [--source <text>] [--agent <name>] | ouro a2a escalation list [--agent <name>]";
1597
+ const action = rest[0];
1598
+ if (action !== "grant" && action !== "revoke" && action !== "list")
1599
+ throw new Error(usageText);
1600
+ let friendId;
1601
+ let source;
1602
+ for (let i = 1; i < rest.length; i += 1) {
1603
+ if (rest[i] === "--friend" && rest[i + 1]) {
1604
+ friendId = rest[++i];
1605
+ continue;
1606
+ }
1607
+ if (rest[i] === "--source" && rest[i + 1]) {
1608
+ source = rest[++i];
1609
+ continue;
1610
+ }
1611
+ throw new Error(usageText);
1612
+ }
1613
+ if ((action === "list") === Boolean(friendId) || (source && action !== "grant"))
1614
+ throw new Error(usageText);
1615
+ return { kind: "a2a.escalation", action, ...(agent ? { agent } : {}), ...(friendId ? { friendId } : {}), ...(source ? { source } : {}) };
1616
+ }
1553
1617
  if (sub === "onboard") {
1554
1618
  let cardUrl;
1555
1619
  let did;
@@ -1612,7 +1676,7 @@ function parseA2ACommand(args) {
1612
1676
  }
1613
1677
  return { kind: "a2a.serve", ...(agent ? { agent } : {}), ...(host ? { host } : {}), ...(port ? { port } : {}), ...(baseUrl ? { baseUrl } : {}), ...(path ? { path } : {}) };
1614
1678
  }
1615
- throw new Error("Usage: ouro a2a card|onboard|serve ...");
1679
+ throw new Error("Usage: ouro a2a card|onboard|serve|message|identity|outbox|escalation ...");
1616
1680
  }
1617
1681
  function parseConfigCommand(args) {
1618
1682
  const { agent, rest: afterAgent } = extractAgentFlag(args);
@@ -60,7 +60,7 @@ function exactSanctuaryContainmentProfileBoundaries(value) {
60
60
  if (!value || typeof value !== "object" || Array.isArray(value))
61
61
  return false;
62
62
  const boundaries = value;
63
- const versions = { "sanctuary-owner": 15, "sanctuary-household": 5, "sanctuary-event": 4 };
63
+ const versions = { "sanctuary-owner": 16, "sanctuary-household": 5, "sanctuary-event": 4 };
64
64
  if (!(0, node_util_1.isDeepStrictEqual)(Object.keys(boundaries).sort(), Object.keys(versions).sort()))
65
65
  return false;
66
66
  const packageRoot = path.resolve(__dirname, "../../../deploy/unraid/sanctuary.ouro");