@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.
@@ -0,0 +1,122 @@
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.escalationGrantsPath = escalationGrantsPath;
37
+ exports.readEscalationGrants = readEscalationGrants;
38
+ exports.setEscalationGrant = setEscalationGrant;
39
+ exports.pinnedHolderDid = pinnedHolderDid;
40
+ exports.holdsEscalation = holdsEscalation;
41
+ exports.escalationHolders = escalationHolders;
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const runtime_1 = require("../nerves/runtime");
45
+ const trusted_files_1 = require("./trusted-files");
46
+ function escalationGrantsPath(agentRoot) {
47
+ return path.join(agentRoot, "state", "a2a", "escalation-grants.json");
48
+ }
49
+ function validGrant(value) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value))
51
+ return false;
52
+ const grant = value;
53
+ return Object.keys(grant).sort().join(",") === "did,grantedAt,scope,source"
54
+ && typeof grant.did === "string" && grant.did.startsWith("did:") && grant.did.length <= 512
55
+ && grant.scope === "escalation"
56
+ && typeof grant.grantedAt === "string" && !Number.isNaN(Date.parse(grant.grantedAt))
57
+ && typeof grant.source === "string" && grant.source.trim().length > 0;
58
+ }
59
+ /** Every well-formed grant in the file, keyed by friend id. Anything else in the file is ignored. */
60
+ function readEscalationGrants(agentRoot) {
61
+ const file = escalationGrantsPath(agentRoot);
62
+ if (!(0, trusted_files_1.isTrustedDirectory)(path.dirname(file)))
63
+ return {};
64
+ const parsed = (0, trusted_files_1.readTrustedJson)(file);
65
+ const grants = parsed?.grants;
66
+ if (!grants || typeof grants !== "object" || Array.isArray(grants))
67
+ return {};
68
+ return Object.fromEntries(Object.entries(grants).filter((entry) => validGrant(entry[1])));
69
+ }
70
+ /** Run as root, the operator's grant leaves the file and its directory root-owned and closed to everyone else, even if an earlier recursive chown had handed them to the resident. */
71
+ function ownAsRoot(target, mode) {
72
+ fs.chownSync(target, 0, 0);
73
+ fs.chmodSync(target, mode);
74
+ }
75
+ /** Writes or removes one grant. The previous file is kept beside it as a timestamped backup before the change. */
76
+ function setEscalationGrant(agentRoot, friendId, change, now = new Date()) {
77
+ const file = escalationGrantsPath(agentRoot);
78
+ // Root repairs the directory first; a grants file the resident owns stays untrusted and is replaced, never blessed.
79
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o755 });
80
+ if (process.geteuid?.() === 0)
81
+ ownAsRoot(path.dirname(file), 0o755);
82
+ const current = readEscalationGrants(agentRoot);
83
+ const had = current[friendId];
84
+ if (change.grant ? had?.did === change.did : had === undefined)
85
+ return { changed: false, backup: null };
86
+ let backup = null;
87
+ if (fs.existsSync(file)) {
88
+ backup = `${file}.bak-${now.toISOString().replace(/[:.]/gu, "-")}`;
89
+ fs.copyFileSync(file, backup);
90
+ }
91
+ const next = { ...current };
92
+ if (change.grant)
93
+ next[friendId] = { scope: "escalation", grantedAt: now.toISOString(), source: change.source, did: change.did };
94
+ else
95
+ delete next[friendId];
96
+ const tmp = `${file}.${process.pid}.tmp`;
97
+ fs.writeFileSync(tmp, `${JSON.stringify({ schemaVersion: 1, grants: next }, null, 2)}\n`, { mode: 0o644 });
98
+ fs.renameSync(tmp, file);
99
+ if (process.geteuid?.() === 0)
100
+ ownAsRoot(file, 0o644);
101
+ (0, runtime_1.emitNervesEvent)({
102
+ component: "senses",
103
+ event: "senses.a2a_escalation_grant_changed",
104
+ message: change.grant ? "granted escalation to an A2A peer" : "revoked an A2A peer's escalation grant",
105
+ meta: { friendId, granted: change.grant },
106
+ });
107
+ return { changed: true, backup };
108
+ }
109
+ /** The DID the operator pinned for this holder, or null when it holds no (complete) grant. */
110
+ function pinnedHolderDid(agentRoot, friendId) {
111
+ return readEscalationGrants(agentRoot)[friendId]?.did ?? null;
112
+ }
113
+ /** True only for an active family friend that the operator listed in the grants file, with a pinned DID. */
114
+ function holdsEscalation(agentRoot, friend) {
115
+ return readEscalationGrants(agentRoot)[friend.id] !== undefined
116
+ && friend.trustLevel === "family"
117
+ && friend.admissionState === "active";
118
+ }
119
+ async function escalationHolders(agentRoot, store) {
120
+ const friends = await store.listAll?.() ?? [];
121
+ return friends.filter((friend) => holdsEscalation(agentRoot, friend));
122
+ }
@@ -0,0 +1,60 @@
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.RUNNING_HARNESS_VERSION = void 0;
37
+ exports.escalationOptionsFor = escalationOptionsFor;
38
+ exports.sendOwnerNoticeViaTelegram = sendOwnerNoticeViaTelegram;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const runtime_1 = require("../nerves/runtime");
42
+ /**
43
+ * The version of the harness that is running, read from its own package.json once when this module loads. A fix is
44
+ * "live" only when this process carries it, so the value must not follow whatever an upgrade later writes to disk.
45
+ */
46
+ exports.RUNNING_HARNESS_VERSION = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf8")).version;
47
+ /**
48
+ * What every A2A server entry point passes as `escalation`: confirm-when-live notices go to the owner through the same
49
+ * owner-notice path as delegated commands. Only the Butler files and confirms failure reports.
50
+ */
51
+ function escalationOptionsFor(agentName, sendOwnerNotice) {
52
+ if (agentName !== "sanctuary")
53
+ return {};
54
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.a2a_escalation_wired", message: "A2A server will confirm resolved failure reports to the owner", meta: { agentName, runningVersion: exports.RUNNING_HARNESS_VERSION } });
55
+ return { escalation: { runningVersion: exports.RUNNING_HARNESS_VERSION, notifyOwner: sendOwnerNotice } };
56
+ }
57
+ async function sendOwnerNoticeViaTelegram(agentName, notice) {
58
+ const { sendTelegramOwnerNotice } = await Promise.resolve().then(() => __importStar(require("../senses/telegram")));
59
+ await sendTelegramOwnerNotice(agentName, { ...notice, signal: AbortSignal.timeout(30_000) });
60
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callOutboxMethod = callOutboxMethod;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
6
+ const client_1 = require("./client");
7
+ const outbox_wire_1 = require("./outbox-wire");
8
+ const sealed_chat_1 = require("./sealed-chat");
9
+ const runtime_1 = require("../nerves/runtime");
10
+ /**
11
+ * Calls one outbox method on an A2A agent as a verified friend. The request is sealed to the agent's card DID and
12
+ * signed by `identity`, and the reply is opened only if it was sealed by that same card key, so neither direction
13
+ * travels in plaintext. Throws with the agent's refusal text when the agent answers with a JSON-RPC error.
14
+ */
15
+ async function callOutboxMethod(input) {
16
+ const fetchImpl = input.fetchImpl ?? fetch;
17
+ const sodium = input.sodium ?? await (0, a2a_client_1.ready)();
18
+ const card = await (0, client_1.fetchA2AAgentCard)(input.cardUrl, fetchImpl);
19
+ const peerDid = (0, client_1.cardDid)(card);
20
+ if (!peerDid)
21
+ throw new Error(`A2A card ${input.cardUrl} serves no DID; outbox calls need one`);
22
+ const parsed = (0, a2a_client_1.parseDidKey)(peerDid);
23
+ if (!parsed || !(0, a2a_client_1.verifyCardDidBinding)({ card: { did: peerDid, url: input.cardUrl }, did: peerDid, didDoc: null })) {
24
+ throw new Error(`A2A card↔DID binding failed for ${peerDid}`);
25
+ }
26
+ const endpointUrl = (0, client_1.endpointForCard)(card);
27
+ const conversationId = (0, node_crypto_1.randomUUID)();
28
+ const sealed = (0, sealed_chat_1.sealChatMessage)({
29
+ sodium, from: input.identity, recipientDid: peerDid, recipientEd25519Pub: parsed.ed25519Pub,
30
+ text: (0, outbox_wire_1.encodeOutboxCommand)(input.method, input.params), conversationId,
31
+ });
32
+ const request = {
33
+ jsonrpc: "2.0", id: (0, node_crypto_1.randomUUID)(), method: input.method,
34
+ params: { message: { kind: "message", role: "ROLE_USER", messageId: (0, node_crypto_1.randomUUID)(), contextId: conversationId, parts: sealed.parts } },
35
+ };
36
+ (0, runtime_1.emitNervesEvent)({ component: "channels", event: "channel.a2a_outbox_call_start", message: "calling an A2A outbox method", meta: { endpointUrl, peerDid, method: input.method } });
37
+ const rpc = await (0, client_1.postJsonRpc)(endpointUrl, request, fetchImpl);
38
+ if ("error" in rpc)
39
+ throw new Error(`A2A error ${rpc.error.code}: ${rpc.error.message}`);
40
+ const reply = rpc.result;
41
+ if (!reply || !Array.isArray(reply.parts))
42
+ throw new Error("A2A outbox call returned no reply message");
43
+ const opened = await (0, sealed_chat_1.openChatMessage)({ sodium, self: input.identity, message: reply, senderDid: peerDid, senderEd25519Pub: parsed.ed25519Pub });
44
+ if (!opened.ok)
45
+ throw new Error(`A2A outbox reply rejected: ${opened.reason}`);
46
+ let result;
47
+ try {
48
+ result = JSON.parse(opened.text);
49
+ }
50
+ catch {
51
+ throw new Error("A2A outbox reply was not JSON");
52
+ }
53
+ if (!result || typeof result !== "object" || Array.isArray(result))
54
+ throw new Error("A2A outbox reply was not a JSON object");
55
+ (0, runtime_1.emitNervesEvent)({ component: "channels", event: "channel.a2a_outbox_call_end", message: "received an A2A outbox reply", meta: { endpointUrl, peerDid, method: input.method } });
56
+ return result;
57
+ }
@@ -0,0 +1,188 @@
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.FileOutboxStore = exports.OUTBOX_LIST_MAX_CHARS = exports.OUTBOX_LIST_MAX_LIMIT = exports.OUTBOX_LIST_DEFAULT_LIMIT = exports.OUTBOX_MAX_BODY_CHARS = exports.OUTBOX_MAX_ENTRIES = void 0;
37
+ exports.isOutboxEntryId = isOutboxEntryId;
38
+ exports.outboxRoot = outboxRoot;
39
+ const node_crypto_1 = require("node:crypto");
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const file_lock_1 = require("../heart/file-lock");
43
+ const runtime_1 = require("../nerves/runtime");
44
+ exports.OUTBOX_MAX_ENTRIES = 500;
45
+ exports.OUTBOX_MAX_BODY_CHARS = 6_000;
46
+ exports.OUTBOX_LIST_DEFAULT_LIMIT = 20;
47
+ exports.OUTBOX_LIST_MAX_LIMIT = 50;
48
+ /** One listing reply is a single sealed message of at most 16,000 characters, so a listing stops filling well before that. */
49
+ exports.OUTBOX_LIST_MAX_CHARS = 12_000;
50
+ const FRIEND_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
51
+ const ENTRY_ID = /^\d{13}-[0-9a-f]{6}$/u;
52
+ function isOutboxEntryId(value) {
53
+ return typeof value === "string" && ENTRY_ID.test(value);
54
+ }
55
+ function entryDir(agentRoot, friendId) {
56
+ if (!FRIEND_ID.test(friendId))
57
+ throw new Error("outbox friend id is invalid");
58
+ return path.join(agentRoot, "state", "outbox", friendId);
59
+ }
60
+ function outboxRoot(agentRoot) {
61
+ return path.join(agentRoot, "state", "outbox");
62
+ }
63
+ function readEntry(file) {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
66
+ if (!isOutboxEntryId(parsed.id) || typeof parsed.kind !== "string" || typeof parsed.createdAt !== "string" || typeof parsed.body !== "string")
67
+ return null;
68
+ return { id: parsed.id, kind: parsed.kind, createdAt: parsed.createdAt, body: parsed.body, ...(parsed.meta ? { meta: parsed.meta } : {}) };
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function sortedIds(dir) {
75
+ let names;
76
+ try {
77
+ names = fs.readdirSync(dir);
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ return names.filter((name) => name.endsWith(".json") && isOutboxEntryId(name.slice(0, -5))).map((name) => name.slice(0, -5)).sort();
83
+ }
84
+ class FileOutboxStore {
85
+ agentRoot;
86
+ constructor(agentRoot) {
87
+ this.agentRoot = agentRoot;
88
+ }
89
+ /** Adds one entry for `friendId`. The oldest entries beyond the cap are dropped so a peer that never reads cannot fill the disk. */
90
+ append(friendId, input, now = Date.now()) {
91
+ const dir = entryDir(this.agentRoot, friendId);
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ const id = `${String(now).padStart(13, "0")}-${(0, node_crypto_1.randomBytes)(3).toString("hex")}`;
94
+ const entry = {
95
+ id,
96
+ kind: input.kind,
97
+ createdAt: new Date(now).toISOString(),
98
+ body: input.body.slice(0, exports.OUTBOX_MAX_BODY_CHARS),
99
+ ...(input.meta ? { meta: input.meta } : {}),
100
+ };
101
+ const file = path.join(dir, `${id}.json`);
102
+ const tmp = `${file}.${process.pid}.tmp`;
103
+ fs.writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}\n`, { mode: 0o600 });
104
+ fs.renameSync(tmp, file);
105
+ const ids = sortedIds(dir);
106
+ const evicted = ids.slice(0, Math.max(0, ids.length - exports.OUTBOX_MAX_ENTRIES));
107
+ for (const stale of evicted)
108
+ fs.rmSync(path.join(dir, `${stale}.json`), { force: true });
109
+ if (evicted.length > 0) {
110
+ (0, runtime_1.emitNervesEvent)({
111
+ level: "warn",
112
+ component: "senses",
113
+ event: "senses.a2a_outbox_evicted",
114
+ message: "a peer outbox is full, so its oldest unread entries were dropped",
115
+ meta: { friendId, evicted: evicted.length, oldestDropped: evicted[0], cap: exports.OUTBOX_MAX_ENTRIES },
116
+ });
117
+ }
118
+ (0, runtime_1.emitNervesEvent)({
119
+ component: "senses",
120
+ event: "senses.a2a_outbox_appended",
121
+ message: "queued a message in a peer outbox",
122
+ meta: { friendId, entryId: id, kind: input.kind },
123
+ });
124
+ return entry;
125
+ }
126
+ /** Like `append`, but a second call with the same `dedupeKey` while the first entry is still unacked returns that entry, so a retried delivery cannot double-post. */
127
+ async appendOnce(friendId, dedupeKey, input, now = Date.now()) {
128
+ const dir = entryDir(this.agentRoot, friendId);
129
+ // The check and the append happen under one cross-process lock, so two senses retrying the same delivery cannot both miss it.
130
+ return (0, file_lock_1.withFileLock)(path.join(this.agentRoot, "state", "outbox", ".locks"), `once-${friendId}`, () => {
131
+ for (const id of sortedIds(dir)) {
132
+ const entry = readEntry(path.join(dir, `${id}.json`));
133
+ if (entry?.meta?.dedupeKey === dedupeKey)
134
+ return entry;
135
+ }
136
+ return this.append(friendId, { ...input, meta: { ...input.meta, dedupeKey } }, now);
137
+ });
138
+ }
139
+ /** Entries after `since` (exclusive), oldest first, within the count and size limits. */
140
+ list(friendId, options = {}) {
141
+ const dir = entryDir(this.agentRoot, friendId);
142
+ const limit = Math.min(Math.max(1, Math.floor(options.limit ?? exports.OUTBOX_LIST_DEFAULT_LIMIT)), exports.OUTBOX_LIST_MAX_LIMIT);
143
+ const since = options.since ?? "";
144
+ const ids = sortedIds(dir).filter((id) => id > since);
145
+ const entries = [];
146
+ let chars = 0;
147
+ let more = false;
148
+ for (const id of ids) {
149
+ const entry = readEntry(path.join(dir, `${id}.json`));
150
+ if (!entry)
151
+ continue;
152
+ const size = JSON.stringify(entry).length;
153
+ if (entries.length >= limit || (entries.length > 0 && chars + size > exports.OUTBOX_LIST_MAX_CHARS)) {
154
+ more = true;
155
+ break;
156
+ }
157
+ entries.push(entry);
158
+ chars += size;
159
+ }
160
+ return { entries, nextCursor: entries.length > 0 ? entries[entries.length - 1].id : null, more };
161
+ }
162
+ /** Removes the named entries from this friend's outbox only; ids that are malformed or not there come back as unknown. */
163
+ ack(friendId, ids) {
164
+ const dir = entryDir(this.agentRoot, friendId);
165
+ const acked = [];
166
+ const unknown = [];
167
+ for (const id of ids) {
168
+ const file = isOutboxEntryId(id) ? path.join(dir, `${id}.json`) : null;
169
+ if (file && fs.existsSync(file)) {
170
+ fs.rmSync(file, { force: true });
171
+ acked.push(id);
172
+ }
173
+ else {
174
+ unknown.push(id);
175
+ }
176
+ }
177
+ if (acked.length > 0) {
178
+ (0, runtime_1.emitNervesEvent)({
179
+ component: "senses",
180
+ event: "senses.a2a_outbox_acked",
181
+ message: "a peer cleared entries from its outbox",
182
+ meta: { friendId, count: acked.length },
183
+ });
184
+ }
185
+ return { acked, unknown };
186
+ }
187
+ }
188
+ exports.FileOutboxStore = FileOutboxStore;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OUTBOX_ERROR_INVALID = exports.OUTBOX_ERROR_REFUSED = exports.OUTBOX_METHODS = void 0;
4
+ exports.isOutboxMethod = isOutboxMethod;
5
+ exports.encodeOutboxCommand = encodeOutboxCommand;
6
+ exports.parseOutboxCommand = parseOutboxCommand;
7
+ exports.handleOutboxCommand = handleOutboxCommand;
8
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
9
+ const escalation_grants_1 = require("./escalation-grants");
10
+ const resolution_proof_1 = require("./resolution-proof");
11
+ const outbox_store_1 = require("./outbox-store");
12
+ const failure_reports_1 = require("../heart/failure-reports");
13
+ const runtime_1 = require("../nerves/runtime");
14
+ /**
15
+ * The outbox methods ride the same sealed, signed chat envelope as `a2a message`: the request is a `message` whose
16
+ * signed text is `{"ouro":"a2a-method","method":...,"params":...}`, posted under the JSON-RPC method of the same name.
17
+ * The server verifies the sender exactly as it does for a chat (unseal, pinned key, signature, replay guard, friend
18
+ * record), requires the signed method to equal the JSON-RPC method, and then decides access itself from the verified
19
+ * friend record. A sender can only ever name its own outbox: no parameter selects another peer's.
20
+ *
21
+ * outbox/list { since?: string, limit?: number } -> { entries, nextCursor, more } any active friend, own outbox
22
+ * outbox/ack { ids: string[] } -> { acked, unknown } any active friend, own outbox
23
+ * report/resolve { id, version, note, resolvedAt, proof } -> { id, status: "resolved" } escalation holders, own reports
24
+ */
25
+ exports.OUTBOX_METHODS = ["outbox/list", "outbox/ack", "report/resolve"];
26
+ exports.OUTBOX_ERROR_REFUSED = -32003;
27
+ exports.OUTBOX_ERROR_INVALID = -32602;
28
+ const MAX_ACK_IDS = 100;
29
+ function isOutboxMethod(method) {
30
+ return typeof method === "string" && exports.OUTBOX_METHODS.includes(method);
31
+ }
32
+ function encodeOutboxCommand(method, params) {
33
+ return JSON.stringify({ ouro: "a2a-method", method, params });
34
+ }
35
+ function parseOutboxCommand(text) {
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(text);
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
44
+ return null;
45
+ const command = parsed;
46
+ if (command.ouro !== "a2a-method" || !isOutboxMethod(command.method))
47
+ return null;
48
+ const params = command.params === undefined ? {} : command.params;
49
+ if (!params || typeof params !== "object" || Array.isArray(params))
50
+ return null;
51
+ return { method: command.method, params: params };
52
+ }
53
+ const refuse = (message) => ({ ok: false, code: exports.OUTBOX_ERROR_REFUSED, message });
54
+ const invalid = (message) => ({ ok: false, code: exports.OUTBOX_ERROR_INVALID, message });
55
+ /** Runs one verified outbox command for `friend`. `friend` must be the record found by the signed sender DID. */
56
+ async function handleOutboxCommand(input) {
57
+ const { agentRoot, friend, method, params } = input;
58
+ if (!friend || friend.admissionState !== "active") {
59
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_outbox_refused", message: "refused an outbox call from a peer that is not an active friend", meta: { method, friendId: friend?.id ?? null } });
60
+ return refuse("only an active friend can use its outbox");
61
+ }
62
+ const outbox = new outbox_store_1.FileOutboxStore(agentRoot);
63
+ if (method === "outbox/list") {
64
+ const { since, limit } = params;
65
+ if (since !== undefined && typeof since !== "string")
66
+ return invalid("since must be a string cursor");
67
+ if (limit !== undefined && (typeof limit !== "number" || !Number.isFinite(limit)))
68
+ return invalid("limit must be a number");
69
+ return { ok: true, result: { ...outbox.list(friend.id, { ...(since !== undefined ? { since } : {}), ...(limit !== undefined ? { limit: Math.min(limit, outbox_store_1.OUTBOX_LIST_MAX_LIMIT) } : {}) }) } };
70
+ }
71
+ if (method === "outbox/ack") {
72
+ const { ids } = params;
73
+ if (!Array.isArray(ids) || ids.length === 0 || ids.length > MAX_ACK_IDS || ids.some((id) => typeof id !== "string"))
74
+ return invalid(`ids must be 1 to ${MAX_ACK_IDS} strings`);
75
+ return { ok: true, result: { ...outbox.ack(friend.id, ids) } };
76
+ }
77
+ if (!(0, escalation_grants_1.holdsEscalation)(agentRoot, friend)) {
78
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_report_resolve_refused", message: "refused report/resolve from a peer without the escalation grant", meta: { friendId: friend.id } });
79
+ return refuse("report/resolve needs the escalation grant");
80
+ }
81
+ const { id, version, note, resolvedAt, proof } = params;
82
+ if (typeof id !== "string" || typeof version !== "string" || typeof note !== "string" || typeof resolvedAt !== "string")
83
+ return invalid("id, version, note and resolvedAt must be strings");
84
+ // The key is the DID the operator pinned in the root-owned grant, never the friend record; and the A2A-verified caller must be that same DID.
85
+ const holderDid = (0, escalation_grants_1.pinnedHolderDid)(agentRoot, friend.id);
86
+ if (!holderDid || input.verifiedDid !== holderDid) {
87
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_report_resolve_refused", message: "refused report/resolve from a caller whose verified DID is not the pinned holder DID", meta: { friendId: friend.id } });
88
+ return refuse("report not resolved: the caller is not the pinned escalation holder");
89
+ }
90
+ const checked = (0, resolution_proof_1.verifyResolution)({ sodium: await (0, a2a_client_1.ready)(), claim: { reportId: id, version, note, resolvedAt }, proof, holderDid });
91
+ if (!checked.ok)
92
+ return refuse(`report not resolved: the resolution is not signed by your key (${checked.reason})`);
93
+ const resolved = await (0, failure_reports_1.resolveFailureReport)(agentRoot, { id, version, note, byFriendId: friend.id, resolvedAt, proof }, input.now);
94
+ return resolved.ok ? { ok: true, result: { id: resolved.id, status: resolved.status } } : refuse(`report not resolved: ${resolved.reason}`);
95
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signResolution = signResolution;
4
+ exports.friendDid = friendDid;
5
+ exports.verifyResolution = verifyResolution;
6
+ const a2a_client_1 = require("@ouro.bot/friends/a2a-client");
7
+ const friends_1 = require("@ouro.bot/friends");
8
+ const runtime_1 = require("../nerves/runtime");
9
+ /**
10
+ * The proof that an escalation holder, and nobody who can write the Butler's files, said a failure report is fixed.
11
+ * The holder signs `{reportId, version, note, resolvedAt}` with its DID key (canonical JSON, detached Ed25519), and
12
+ * the Butler keeps the signature next to the resolution. The holder's private key never leaves the holder's machine,
13
+ * so a forged report file cannot carry a valid one.
14
+ */
15
+ const PURPOSE = "ouro.report-resolve.v1";
16
+ function envelopeOf(claim) {
17
+ return { purpose: PURPOSE, reportId: claim.reportId, version: claim.version, note: claim.note, resolvedAt: claim.resolvedAt };
18
+ }
19
+ function signResolution(input) {
20
+ const { claim } = input;
21
+ return (0, a2a_client_1.signEnvelope)({
22
+ sodium: input.sodium,
23
+ envelope: envelopeOf(claim),
24
+ signerEd25519Priv: input.identity.ed25519Priv,
25
+ signerDid: input.identity.did,
26
+ signerKeyId: input.identity.keyId,
27
+ });
28
+ }
29
+ /** The DID a friend record names. Used only by the operator command that pins it into the grant; never to verify a resolution. */
30
+ function friendDid(friend) {
31
+ return (0, friends_1.resolveAgentIdentity)(friend.agentMeta).did ?? null;
32
+ }
33
+ function verifyResolution(input) {
34
+ const fail = (reason) => {
35
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.a2a_resolution_proof_rejected", message: "rejected a report resolution without a valid holder signature", meta: { reportId: input.claim.reportId, reason } });
36
+ return { ok: false, reason };
37
+ };
38
+ const proof = input.proof;
39
+ if (!proof || typeof proof !== "object" || Array.isArray(proof))
40
+ return fail("no_proof");
41
+ if (!input.holderDid)
42
+ return fail("no_holder_did");
43
+ const parsed = (0, a2a_client_1.parseDidKey)(input.holderDid);
44
+ if (!parsed || proof.signerDid !== input.holderDid)
45
+ return fail("wrong_signer");
46
+ const valid = (0, a2a_client_1.verifyEnvelopeSignature)({
47
+ sodium: input.sodium,
48
+ envelope: envelopeOf(input.claim),
49
+ proof: proof,
50
+ signerEd25519Pub: parsed.ed25519Pub,
51
+ });
52
+ return valid ? { ok: true } : fail("bad_signature");
53
+ }