@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.
Files changed (32) hide show
  1. package/changelog.json +12 -0
  2. package/deploy/unraid/sanctuary-butler-upgrade.mjs +23 -0
  3. package/deploy/unraid/sanctuary-replay-gate.mjs +182 -22
  4. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  5. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +2 -0
  6. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +4 -2
  7. package/deploy/unraid/sanctuary.xml +1 -1
  8. package/dist/a2a/client.js +1 -0
  9. package/dist/a2a/escalation-grants.js +122 -0
  10. package/dist/a2a/escalation-wiring.js +60 -0
  11. package/dist/a2a/outbox-client.js +57 -0
  12. package/dist/a2a/outbox-store.js +188 -0
  13. package/dist/a2a/outbox-wire.js +95 -0
  14. package/dist/a2a/resolution-proof.js +53 -0
  15. package/dist/a2a/server.js +66 -0
  16. package/dist/a2a/trusted-files.js +98 -0
  17. package/dist/heart/awaiting/a2a-await-delivery.js +18 -0
  18. package/dist/heart/daemon/cli-exec.js +56 -2
  19. package/dist/heart/daemon/cli-parse.js +65 -1
  20. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
  21. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +33 -0
  22. package/dist/heart/failure-reports.js +335 -0
  23. package/dist/heart/file-lock.js +109 -0
  24. package/dist/repertoire/guardrails.js +4 -0
  25. package/dist/repertoire/tools-awaiting.js +33 -2
  26. package/dist/repertoire/tools-base.js +2 -0
  27. package/dist/repertoire/tools-escalation.js +84 -0
  28. package/dist/repertoire/tools.js +1 -0
  29. package/dist/senses/a2a-entry.js +3 -1
  30. package/dist/senses/private-runtime.js +31 -14
  31. package/npm-shrinkwrap.json +2 -2
  32. 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
- return (entry.filer ?? null) === filer && typeof entry.at === "string" && now - Date.parse(entry.at) < ASK_OWNER_WINDOW_MS;
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
+ };
@@ -56,6 +56,7 @@ const SANCTUARY_RELATIONSHIP_BASE_TOOLS = new Set([
56
56
  "query_cares",
57
57
  "care_manage",
58
58
  "await_condition",
59
+ "report_failure",
59
60
  "resolve_await",
60
61
  "cancel_await",
61
62
  "list_recent_attachments",
@@ -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
- const subjectFriend = coordinates.channel === "a2a" ? await resolveA2APrincipal(agentRoot, store, friend, coordinates.requestId) : friend;
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
- : coordinates.channel === "a2a"
193
- // The follow-up carries the delegating owner's authority (as the delegated command did), but the turn profile
194
- // is the peer's own, which can only narrow it: never wider than a peer chat turn or the original command.
195
- ? (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend: subjectFriend, registry, profileId: friend.capabilityProfileId, requestId: coordinates.requestId, requestPhase: "follow_up" })
196
- : (0, relationship_authorization_2.createRelationshipAuthorizationEvaluator)({ friend, registry, requestId: coordinates.requestId, requestPhase: "follow_up" });
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
- : (0, tools_awaiting_1.inspectRelationshipFollowUp)(agentRoot, {
1237
- friendId: relationshipAwaitCoordinatesValue.friendId,
1238
- channel: relationshipAwaitCoordinatesValue.channel,
1239
- key: relationshipAwaitCoordinatesValue.key,
1240
- requestId: relationshipAwaitCoordinatesValue.requestId,
1241
- awaitName: parsedAwait.name,
1242
- now: now().getTime(),
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);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.877",
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.877",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.877",
3
+ "version": "0.1.0-alpha.879",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },