@ouro.bot/cli 0.1.0-alpha.877 → 0.1.0-alpha.879
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 +12 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +23 -0
- package/deploy/unraid/sanctuary-replay-gate.mjs +182 -22
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +2 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +4 -2
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/a2a/client.js +1 -0
- package/dist/a2a/escalation-grants.js +122 -0
- package/dist/a2a/escalation-wiring.js +60 -0
- package/dist/a2a/outbox-client.js +57 -0
- package/dist/a2a/outbox-store.js +188 -0
- package/dist/a2a/outbox-wire.js +95 -0
- package/dist/a2a/resolution-proof.js +53 -0
- package/dist/a2a/server.js +66 -0
- package/dist/a2a/trusted-files.js +98 -0
- package/dist/heart/awaiting/a2a-await-delivery.js +18 -0
- package/dist/heart/daemon/cli-exec.js +56 -2
- package/dist/heart/daemon/cli-parse.js +65 -1
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
- package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +33 -0
- package/dist/heart/failure-reports.js +335 -0
- package/dist/heart/file-lock.js +109 -0
- package/dist/repertoire/guardrails.js +4 -0
- package/dist/repertoire/tools-awaiting.js +33 -2
- package/dist/repertoire/tools-base.js +2 -0
- package/dist/repertoire/tools-escalation.js +84 -0
- package/dist/repertoire/tools.js +1 -0
- package/dist/senses/a2a-entry.js +3 -1
- package/dist/senses/private-runtime.js +31 -14
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
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.withFileLock = withFileLock;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const runtime_1 = require("../nerves/runtime");
|
|
40
|
+
const DEFAULT_STALE_MS = 120_000;
|
|
41
|
+
const DEFAULT_WAIT_MS = 45_000;
|
|
42
|
+
const DEFAULT_POLL_MS = 25;
|
|
43
|
+
/**
|
|
44
|
+
* Breaks a stale lock by renaming it to a name only this caller knows, then removing that. Two waiters both seeing the
|
|
45
|
+
* same stale lock cannot both break it: only one rename succeeds, and the loser never touches a lock a third process
|
|
46
|
+
* has since taken under the original name.
|
|
47
|
+
*/
|
|
48
|
+
function breakStaleLock(lock) {
|
|
49
|
+
const broken = `${lock}.broken-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
50
|
+
try {
|
|
51
|
+
fs.renameSync(lock, broken);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* v8 ignore next -- the loser of a break race finds the lock already renamed away, and simply retries @preserve */
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
removeLock(broken);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** The lock directory is always empty, so removing it never recurses; another process may already have broken it as stale. */
|
|
61
|
+
function removeLock(lock) {
|
|
62
|
+
try {
|
|
63
|
+
fs.rmdirSync(lock);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* v8 ignore next -- a lock broken as stale by another process is already gone @preserve */
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Runs `fn` while holding a named lock in `dir`. The lock is a directory, because creating one is atomic across
|
|
72
|
+
* processes, which matters here: the Butler's senses (Telegram, A2A, the private runtime) are separate processes that
|
|
73
|
+
* share one bundle on disk.
|
|
74
|
+
*/
|
|
75
|
+
async function withFileLock(dir, name, fn, options = {}) {
|
|
76
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
77
|
+
const waitMs = options.waitMs ?? DEFAULT_WAIT_MS;
|
|
78
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
const lock = path.join(dir, `${name}.lock`);
|
|
81
|
+
const deadline = Date.now() + waitMs;
|
|
82
|
+
for (;;) {
|
|
83
|
+
try {
|
|
84
|
+
fs.mkdirSync(lock);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== "EEXIST")
|
|
89
|
+
throw error;
|
|
90
|
+
/* v8 ignore next -- a holder releasing between the failed mkdir and this stat reads as an old lock, which is then simply retried @preserve */
|
|
91
|
+
const heldSince = fs.statSync(lock, { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
92
|
+
if (Date.now() - heldSince > staleMs) {
|
|
93
|
+
/* v8 ignore next -- only the waiter whose rename won reports the break @preserve */
|
|
94
|
+
if (breakStaleLock(lock))
|
|
95
|
+
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "heart", event: "heart.file_lock_broken", message: "broke a lock whose holder never released it", meta: { name, heldMs: Date.now() - heldSince } });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (Date.now() >= deadline)
|
|
99
|
+
throw new Error(`timed out waiting for the ${name} lock`);
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return await fn();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
removeLock(lock);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -63,6 +63,10 @@ const PROTECTED_PATH_SEGMENTS = [
|
|
|
63
63
|
".ouro-cli/vault-unlock/",
|
|
64
64
|
".ouro-cli/vault-unlock-dpapi/",
|
|
65
65
|
"state/policy/",
|
|
66
|
+
// What the Butler tells its escalation peers, and who may hear it: only the harness writes these, never the model.
|
|
67
|
+
"state/a2a/escalation-grants.json",
|
|
68
|
+
"state/outbox/",
|
|
69
|
+
"state/reports/",
|
|
66
70
|
];
|
|
67
71
|
const PROTECTED_FILENAMES = ["agent.json", "tool-profiles.json"];
|
|
68
72
|
/** The agent's psyche folder ships only from the repository (a pull request and the replay gate), so no tool writes into it. */
|
|
@@ -43,6 +43,7 @@ exports.cancelStaleAwait = cancelStaleAwait;
|
|
|
43
43
|
exports.isBrokenBindingReason = isBrokenBindingReason;
|
|
44
44
|
exports.failBrokenAwait = failBrokenAwait;
|
|
45
45
|
exports.inspectRelationshipFollowUp = inspectRelationshipFollowUp;
|
|
46
|
+
exports.inspectPeerAwait = inspectPeerAwait;
|
|
46
47
|
exports.hasActiveRelationshipFollowUp = hasActiveRelationshipFollowUp;
|
|
47
48
|
exports.hasActiveExternalEventAwait = hasActiveExternalEventAwait;
|
|
48
49
|
exports.inspectExternalEventAwait = inspectExternalEventAwait;
|
|
@@ -180,6 +181,8 @@ function failedAskOwner(agentName, name, error) {
|
|
|
180
181
|
/** At most this many ask_owner closures per requester in a rolling day, so one requester cannot make the Butler pester the owner. */
|
|
181
182
|
const ASK_OWNER_DAILY_LIMIT = 3;
|
|
182
183
|
const ASK_OWNER_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
184
|
+
/** Across every requester who is not the owner, so many requesters together cannot flood the owner either. */
|
|
185
|
+
const ASK_OWNER_NON_OWNER_DAILY_LIMIT = 6;
|
|
183
186
|
function askLedgerPath(agentRoot) {
|
|
184
187
|
return path.join(awaitingDir(agentRoot), ".asks.jsonl");
|
|
185
188
|
}
|
|
@@ -230,7 +233,8 @@ function recentAskCount(agentRoot, filer, now) {
|
|
|
230
233
|
return raw.split("\n").filter((line) => {
|
|
231
234
|
try {
|
|
232
235
|
const entry = JSON.parse(line);
|
|
233
|
-
|
|
236
|
+
const who = filer === undefined ? entry.owner === false : (entry.filer ?? null) === filer;
|
|
237
|
+
return who && typeof entry.at === "string" && now - Date.parse(entry.at) < ASK_OWNER_WINDOW_MS;
|
|
234
238
|
}
|
|
235
239
|
catch {
|
|
236
240
|
return false;
|
|
@@ -274,6 +278,9 @@ async function askOwnerTool(name, observation, question, choices, existing, agen
|
|
|
274
278
|
if (!filer.isOwner && recentAskCount(agentRoot, filerId, Date.now()) >= ASK_OWNER_DAILY_LIMIT) {
|
|
275
279
|
return JSON.stringify({ error: `ask_owner limit reached: this requester has already had my owner asked ${ASK_OWNER_DAILY_LIMIT} times in the last day; do not retry ask_owner, resolve with verdict 'no' with an observation and keep polling` });
|
|
276
280
|
}
|
|
281
|
+
if (!filer.isOwner && recentAskCount(agentRoot, undefined, Date.now()) >= ASK_OWNER_NON_OWNER_DAILY_LIMIT) {
|
|
282
|
+
return JSON.stringify({ error: `ask_owner limit reached: my owner has already been asked ${ASK_OWNER_NON_OWNER_DAILY_LIMIT} times today on behalf of other requesters; do not retry ask_owner, resolve with verdict 'no' with an observation and keep polling` });
|
|
283
|
+
}
|
|
277
284
|
const isA2A = existing.filed_from === "a2a";
|
|
278
285
|
const noticeId = `await:${name}:asked_owner:${String(existing.created_at)}`;
|
|
279
286
|
const question = formatOwnerQuestion(parsed.question, parsed.choices);
|
|
@@ -318,7 +325,7 @@ async function askOwnerTool(name, observation, question, choices, existing, agen
|
|
|
318
325
|
/* v8 ignore next -- defensive: archiveAwait only fails on the file-disappears-mid-call race already covered by v8 ignore inside archiveAwait @preserve */
|
|
319
326
|
if (!archive.ok)
|
|
320
327
|
return JSON.stringify({ error: archive.error });
|
|
321
|
-
recordAsk(agentRoot, { filer: filerId, name, at: askedAt, question: parsed.question });
|
|
328
|
+
recordAsk(agentRoot, { filer: filerId, owner: filer.isOwner, name, at: askedAt, question: parsed.question });
|
|
322
329
|
if (!tellsFriend || delivered(friendNotice))
|
|
323
330
|
fulfillAwaitObligation(agentRoot, archive.file);
|
|
324
331
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -643,6 +650,25 @@ function inspectRelationshipFollowUp(agentRoot, input) {
|
|
|
643
650
|
}
|
|
644
651
|
return { active: true };
|
|
645
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* The binding of an await filed in a plain A2A conversation: it has no request id and no obligation, only the peer's friend
|
|
655
|
+
* id and conversation key as provenance. It is live while it is still pending, was filed by that peer in that conversation,
|
|
656
|
+
* and has not outlived its max age.
|
|
657
|
+
*/
|
|
658
|
+
function inspectPeerAwait(agentRoot, input) {
|
|
659
|
+
const awaiting = readAwaitDefinition(agentRoot, input.awaitName);
|
|
660
|
+
if (!awaiting)
|
|
661
|
+
throw new Error(`Await ${input.awaitName} could not be verified`);
|
|
662
|
+
const matches = awaiting.status === "pending" && !awaiting.request_id && !awaiting.obligation_id
|
|
663
|
+
&& awaiting.filed_for_friend_id === input.friendId && awaiting.filed_from === "a2a" && awaiting.filed_from_key === input.key;
|
|
664
|
+
if (!matches)
|
|
665
|
+
return { active: false, reason: "request obligation binding no longer matches" };
|
|
666
|
+
const maxAge = (0, cadence_1.parseCadenceToMs)(awaiting.max_age);
|
|
667
|
+
if (input.allowElapsed !== true && maxAge !== null && awaiting.created_at && (input.now ?? Date.now()) >= Date.parse(awaiting.created_at) + maxAge) {
|
|
668
|
+
return { active: false, reason: "request obligation await expired" };
|
|
669
|
+
}
|
|
670
|
+
return { active: true };
|
|
671
|
+
}
|
|
646
672
|
function hasActiveRelationshipFollowUp(agentRoot, input) {
|
|
647
673
|
if (input.awaitName)
|
|
648
674
|
return inspectRelationshipFollowUp(agentRoot, { ...input, awaitName: input.awaitName }).active;
|
|
@@ -770,6 +796,11 @@ exports.awaitingToolDefinitions = [
|
|
|
770
796
|
if (session?.channel === "external-event" && existing?.wake_at) {
|
|
771
797
|
binding = inspectExternalEventAwait(agentName, { recordPath: session.key, awaitName, wakeAt: existing.wake_at });
|
|
772
798
|
}
|
|
799
|
+
else if (session?.channel === "a2a" && ctx.awaitTick && !requestId && existing && !existing.request_id
|
|
800
|
+
&& existing.filed_for_friend_id === session.friendId && existing.filed_from === "a2a" && existing.filed_from_key === session.key) {
|
|
801
|
+
// A plain peer await is resolved only by its own tick, never from a chat turn.
|
|
802
|
+
binding = inspectPeerAwait(agentRoot, { friendId: session.friendId, key: session.key, awaitName });
|
|
803
|
+
}
|
|
773
804
|
else if (session && requestId) {
|
|
774
805
|
if (existing?.filed_for_friend_id !== session.friendId || existing.filed_from !== session.channel
|
|
775
806
|
|| existing.filed_from_key !== session.key || existing.request_id !== requestId) {
|
|
@@ -24,6 +24,7 @@ const tools_trip_1 = require("./tools-trip");
|
|
|
24
24
|
const tools_a2a_1 = require("./tools-a2a");
|
|
25
25
|
const tools_commerce_1 = require("./tools-commerce");
|
|
26
26
|
const tools_awaiting_1 = require("./tools-awaiting");
|
|
27
|
+
const tools_escalation_1 = require("./tools-escalation");
|
|
27
28
|
const tools_obligations_1 = require("./tools-obligations");
|
|
28
29
|
const tools_evolution_1 = require("./tools-evolution");
|
|
29
30
|
const tools_runtime_1 = require("./tools-runtime");
|
|
@@ -75,6 +76,7 @@ exports.baseToolDefinitions = [
|
|
|
75
76
|
...tools_a2a_1.a2aToolDefinitions,
|
|
76
77
|
...tools_commerce_1.commerceToolDefinitions,
|
|
77
78
|
...tools_awaiting_1.awaitingToolDefinitions,
|
|
79
|
+
tools_escalation_1.reportFailureToolDefinition,
|
|
78
80
|
...tools_obligations_1.obligationToolDefinitions,
|
|
79
81
|
...tools_evolution_1.evolutionToolDefinitions,
|
|
80
82
|
...tools_orientation_1.orientationToolDefinitions,
|
|
@@ -0,0 +1,84 @@
|
|
|
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.reportFailureToolDefinition = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const friends_1 = require("@ouro.bot/friends");
|
|
39
|
+
const failure_reports_1 = require("../heart/failure-reports");
|
|
40
|
+
const runtime_1 = require("../nerves/runtime");
|
|
41
|
+
function text(value) {
|
|
42
|
+
return typeof value === "string" ? value : "";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Lets the agent hand a failure it cannot work around to the peers who fix it. The session comes from the runtime
|
|
46
|
+
* context, never from the model, so a report cannot be addressed to or attributed to anyone else.
|
|
47
|
+
*/
|
|
48
|
+
exports.reportFailureToolDefinition = {
|
|
49
|
+
tool: {
|
|
50
|
+
type: "function",
|
|
51
|
+
function: {
|
|
52
|
+
name: "report_failure",
|
|
53
|
+
description: "File a failure report for the engineers who fix you. Use it when a tool you need is missing, a tool errors in a way you cannot work around, or you are about to give up on what the owner asked. Then tell the owner you have filed it, with the short report id, in your own words. Do not ask the owner to do anything about it.",
|
|
54
|
+
parameters: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
ari_words: { type: "string", description: "What the owner asked for, verbatim from the conversation." },
|
|
58
|
+
tried: { type: "string", description: "What you tried: tool names and arguments, summarized." },
|
|
59
|
+
error: { type: "string", description: "The tool error, or the reason you gave up." },
|
|
60
|
+
failed_tool: { type: "string", description: "The tool that failed or that you wished existed, if there is one." },
|
|
61
|
+
severity: { type: "string", enum: ["low", "medium", "high"], description: "How much this blocks the owner." },
|
|
62
|
+
},
|
|
63
|
+
required: ["ari_words", "tried", "error"],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
handler: async (args, ctx) => {
|
|
69
|
+
if (!ctx?.agentRoot)
|
|
70
|
+
return "report_failure is unavailable: no agent runtime.";
|
|
71
|
+
const severity = ["low", "medium", "high"].includes(args.severity) ? args.severity : "medium";
|
|
72
|
+
const store = new friends_1.FileFriendStore(path.join(ctx.agentRoot, "friends"));
|
|
73
|
+
const result = await (0, failure_reports_1.fileFailureReport)(ctx.agentRoot, store, {
|
|
74
|
+
ariWords: text(args.ari_words), tried: text(args.tried), error: text(args.error),
|
|
75
|
+
...(text(args.failed_tool) ? { failedTool: text(args.failed_tool) } : {}),
|
|
76
|
+
severity,
|
|
77
|
+
origin: { friendId: ctx.currentSession?.friendId ?? null, channel: ctx.currentSession?.channel ?? null, key: ctx.currentSession?.key ?? null },
|
|
78
|
+
});
|
|
79
|
+
(0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.report_failure", message: "failure report requested", meta: { ok: result.ok, ...(result.ok ? { duplicate: result.duplicate } : { reason: result.reason }) } });
|
|
80
|
+
if (!result.ok)
|
|
81
|
+
return `Not filed (${result.reason}): ${result.detail}`;
|
|
82
|
+
return JSON.stringify({ filed: true, reportId: result.id, shortId: result.shortId, duplicate: result.duplicate, recipients: result.recipients });
|
|
83
|
+
},
|
|
84
|
+
};
|
package/dist/repertoire/tools.js
CHANGED
package/dist/senses/a2a-entry.js
CHANGED
|
@@ -61,8 +61,9 @@ Promise.all([
|
|
|
61
61
|
Promise.resolve().then(() => __importStar(require("../heart/runtime-credentials"))),
|
|
62
62
|
Promise.resolve().then(() => __importStar(require("../heart/machine-identity"))),
|
|
63
63
|
Promise.resolve().then(() => __importStar(require("@ouro.bot/friends/a2a-client"))),
|
|
64
|
+
Promise.resolve().then(() => __importStar(require("../a2a/escalation-wiring"))),
|
|
64
65
|
])
|
|
65
|
-
.then(async ([{ startA2AServer }, { loadOrMintA2AIdentity, readStoredA2ASeed }, { waitForRuntimeCredentialBootstrap, readMachineRuntimeCredentialConfig, refreshMachineRuntimeCredentialConfig, mergeMachineRuntimeCredentialConfig }, { loadOrCreateMachineIdentity }, { ready },]) => {
|
|
66
|
+
.then(async ([{ startA2AServer }, { loadOrMintA2AIdentity, readStoredA2ASeed }, { waitForRuntimeCredentialBootstrap, readMachineRuntimeCredentialConfig, refreshMachineRuntimeCredentialConfig, mergeMachineRuntimeCredentialConfig }, { loadOrCreateMachineIdentity }, { ready }, { escalationOptionsFor, sendOwnerNoticeViaTelegram },]) => {
|
|
66
67
|
const os = await Promise.resolve().then(() => __importStar(require("node:os")));
|
|
67
68
|
// Wait for the daemon to deliver the machine-local runtime config (carrying the
|
|
68
69
|
// a2a identity seed) into the in-memory cache before we read or mint identity.
|
|
@@ -109,6 +110,7 @@ Promise.all([
|
|
|
109
110
|
},
|
|
110
111
|
},
|
|
111
112
|
} : {}),
|
|
113
|
+
...escalationOptionsFor(agentName, (notice) => sendOwnerNoticeViaTelegram(agentName, notice)),
|
|
112
114
|
...(argValue("--host") ? { host: argValue("--host") } : {}),
|
|
113
115
|
...(Number.isInteger(port) ? { port } : {}),
|
|
114
116
|
...(argValue("--base-url") ? { baseUrl: argValue("--base-url") } : {}),
|
|
@@ -133,6 +133,12 @@ function relationshipAwaitCoordinates(awaitFile) {
|
|
|
133
133
|
throw new InvalidRelationshipAwaitProvenanceError("Relationship await provenance is invalid");
|
|
134
134
|
return { friendId: awaitFile.filed_for_friend_id, channel: awaitFile.filed_from, key: awaitFile.filed_from_key, requestId: null };
|
|
135
135
|
}
|
|
136
|
+
if (awaitFile.filed_from === "a2a" && awaitFile.filed_for_friend_id && awaitFile.filed_from_key) {
|
|
137
|
+
// Filed in a plain (non-delegated) A2A conversation: the peer and conversation are its whole provenance.
|
|
138
|
+
if (Buffer.byteLength(awaitFile.filed_for_friend_id) > 256 || Buffer.byteLength(awaitFile.filed_from_key) > 1_024)
|
|
139
|
+
throw new InvalidRelationshipAwaitProvenanceError("Relationship await provenance is invalid");
|
|
140
|
+
return { friendId: awaitFile.filed_for_friend_id, channel: "a2a", key: awaitFile.filed_from_key, requestId: null };
|
|
141
|
+
}
|
|
136
142
|
const isSystemAwait = awaitFile.filed_for_friend_id === null
|
|
137
143
|
&& awaitFile.filed_from_key === null
|
|
138
144
|
&& (awaitFile.filed_from === null || awaitFile.filed_from === "unknown" || awaitFile.filed_from === "cli");
|
|
@@ -186,14 +192,23 @@ async function resolveRelationshipAwaitAuthority(agentRoot, store, registry, coo
|
|
|
186
192
|
if (coordinates.channel === "external-event" && (friend.capabilityProfileId !== "sanctuary-owner" || !registry.profiles["sanctuary-event"])) {
|
|
187
193
|
throw new StaleRelationshipAwaitError("admission or profile is not active");
|
|
188
194
|
}
|
|
189
|
-
|
|
195
|
+
// A plain peer await carries the peer's own authority and nobody else's: no principal, no delegation grant.
|
|
196
|
+
const plainPeerAwait = coordinates.channel === "a2a" && coordinates.requestId === null;
|
|
197
|
+
// A peer the operator switched to no contact at all is not ticked; every other policy is held to following up on this one internal request.
|
|
198
|
+
if (plainPeerAwait && friend.initiativePolicy === "none")
|
|
199
|
+
throw new StaleRelationshipAwaitError("initiative policy denies contact");
|
|
200
|
+
const subjectFriend = coordinates.channel === "a2a" && !plainPeerAwait ? await resolveA2APrincipal(agentRoot, store, friend, coordinates.requestId) : friend;
|
|
190
201
|
const authorization = coordinates.channel === "external-event"
|
|
191
202
|
? (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend, registry, profileId: "sanctuary-event", requestPhase: "follow_up" })
|
|
192
|
-
:
|
|
193
|
-
// The
|
|
194
|
-
// is
|
|
195
|
-
? (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend:
|
|
196
|
-
:
|
|
203
|
+
: plainPeerAwait
|
|
204
|
+
// The peer's own profile and trust, nothing wider. Following up is reading its own outbox, not contacting it, so the
|
|
205
|
+
// follow-up phase is opened with an internal request id that exists only for this await and is never shown to the peer.
|
|
206
|
+
? (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend: { ...friend, initiativePolicy: "request_follow_up_only" }, registry, profileId: friend.capabilityProfileId, requestId: `peer-await:${coordinates.key}`, requestPhase: "follow_up" })
|
|
207
|
+
: coordinates.channel === "a2a"
|
|
208
|
+
// The follow-up carries the delegating owner's authority (as the delegated command did), but the turn profile
|
|
209
|
+
// is the peer's own, which can only narrow it: never wider than a peer chat turn or the original command.
|
|
210
|
+
? (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend: subjectFriend, registry, profileId: friend.capabilityProfileId, requestId: coordinates.requestId, requestPhase: "follow_up" })
|
|
211
|
+
: (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend, registry, requestId: coordinates.requestId, requestPhase: "follow_up" });
|
|
197
212
|
if (authorization.subject.friendId !== subjectFriend.id)
|
|
198
213
|
throw new Error("Relationship await authority friend is ambiguous");
|
|
199
214
|
if (!authorization.authorizeTool("resolve_await").allowed)
|
|
@@ -1233,14 +1248,16 @@ async function runPrivateRuntimeTurn(options) {
|
|
|
1233
1248
|
? parsedAwait.wake_at && (0, tools_awaiting_1.hasActiveExternalEventAwait)(agentName, { recordPath: relationshipAwaitCoordinatesValue.key, awaitName: parsedAwait.name, wakeAt: parsedAwait.wake_at })
|
|
1234
1249
|
? { active: true }
|
|
1235
1250
|
: { active: false, reason: "external event disposition is no longer active" }
|
|
1236
|
-
:
|
|
1237
|
-
friendId: relationshipAwaitCoordinatesValue.friendId,
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1251
|
+
: relationshipAwaitCoordinatesValue.channel === "a2a" && relationshipAwaitCoordinatesValue.requestId === null
|
|
1252
|
+
? (0, tools_awaiting_1.inspectPeerAwait)(agentRoot, { friendId: relationshipAwaitCoordinatesValue.friendId, key: relationshipAwaitCoordinatesValue.key, awaitName: parsedAwait.name, now: now().getTime() })
|
|
1253
|
+
: (0, tools_awaiting_1.inspectRelationshipFollowUp)(agentRoot, {
|
|
1254
|
+
friendId: relationshipAwaitCoordinatesValue.friendId,
|
|
1255
|
+
channel: relationshipAwaitCoordinatesValue.channel,
|
|
1256
|
+
key: relationshipAwaitCoordinatesValue.key,
|
|
1257
|
+
requestId: relationshipAwaitCoordinatesValue.requestId,
|
|
1258
|
+
awaitName: parsedAwait.name,
|
|
1259
|
+
now: now().getTime(),
|
|
1260
|
+
});
|
|
1244
1261
|
if (!binding.active) {
|
|
1245
1262
|
if ((0, tools_awaiting_1.isBrokenBindingReason)(binding.reason))
|
|
1246
1263
|
(0, tools_awaiting_1.failBrokenAwait)(agentRoot, agentName, parsedAwait.name, binding.reason);
|
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.879",
|
|
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.879",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|