@ouro.bot/cli 0.1.0-alpha.810 → 0.1.0-alpha.811

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 (38) hide show
  1. package/changelog.json +12 -0
  2. package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
  3. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
  4. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  5. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
  6. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  7. package/deploy/unraid/sanctuary.xml +1 -1
  8. package/dist/heart/config.js +32 -2
  9. package/dist/heart/core.js +167 -115
  10. package/dist/heart/daemon/daemon.js +19 -9
  11. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
  12. package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
  13. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
  14. package/dist/heart/frontend-approval-runtime.js +87 -13
  15. package/dist/heart/identity.js +17 -5
  16. package/dist/heart/session-events.js +8 -1
  17. package/dist/heart/tool-approval.js +64 -31
  18. package/dist/mind/pending.js +4 -4
  19. package/dist/repertoire/mcp-manager.js +365 -381
  20. package/dist/repertoire/mcp-tools.js +66 -25
  21. package/dist/repertoire/plugin-mcp.js +3 -3
  22. package/dist/repertoire/shell-sessions.js +8 -7
  23. package/dist/repertoire/tool-arguments.js +30 -8
  24. package/dist/repertoire/tools-session.js +30 -5
  25. package/dist/repertoire/tools-shell.js +30 -14
  26. package/dist/repertoire/tools-voice.js +6 -6
  27. package/dist/repertoire/tools.js +231 -205
  28. package/dist/senses/bluebubbles/index.js +4 -2
  29. package/dist/senses/cli.js +6 -5
  30. package/dist/senses/private-runtime.js +9 -3
  31. package/dist/senses/shared-turn.js +12 -9
  32. package/dist/senses/teams.js +15 -12
  33. package/dist/senses/telegram-approval-runtime.js +129 -20
  34. package/dist/senses/telegram-client.js +19 -2
  35. package/dist/senses/telegram.js +54 -15
  36. package/dist/senses/voice/twilio-phone.js +123 -173
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
@@ -951,7 +951,9 @@ async function main(agentName, options) {
951
951
  // Fail fast if provider is misconfigured (triggers human-readable error + exit)
952
952
  (0, core_1.getProvider)("human");
953
953
  // Resolve context kernel (identity + channel) for CLI
954
- const friendsPath = path.join((0, identity_1.getAgentRoot)(), "friends");
954
+ const currentAgentName = (0, identity_1.getAgentName)();
955
+ const owner = { agentName: currentAgentName, agentRoot: (0, identity_1.getAgentRoot)(currentAgentName) };
956
+ const friendsPath = path.join(owner.agentRoot, "friends");
955
957
  const friendStore = new friends_1.FileFriendStore(friendsPath);
956
958
  const username = os.userInfo().username;
957
959
  const localExternalId = username;
@@ -975,13 +977,11 @@ async function main(agentName, options) {
975
977
  let existing = null;
976
978
  let sessionState;
977
979
  let sessionEvents = [];
978
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
979
980
  const sessionMessages = [
980
981
  { role: "system", content: (0, prompt_1.flattenSystemPrompt)(await (0, prompt_1.buildSystem)("cli", {}, resolvedContext)) },
981
982
  ];
982
983
  // Per-turn pipeline input: CLI capabilities and pending dir
983
984
  const cliCapabilities = (0, friends_1.getChannelCapabilities)("cli");
984
- const currentAgentName = (0, identity_1.getAgentName)();
985
985
  const pendingDir = (0, pending_1.getPendingDir)(currentAgentName, friendId, "cli", "session");
986
986
  const summarize = (0, core_1.createSummarize)("human");
987
987
  const cliFailoverState = { pending: null };
@@ -1077,12 +1077,14 @@ async function main(agentName, options) {
1077
1077
  enforceTrustGate: trust_gate_1.enforceTrustGate,
1078
1078
  drainPending: pending_1.drainPending,
1079
1079
  drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(currentAgentName, deferredFriendId),
1080
- runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
1080
+ runAgent: async (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
1081
1081
  ...opts,
1082
+ mcpManager: opts?.hardDisableTools ? undefined : await (0, mcp_manager_1.getSharedMcpManager)(owner) ?? undefined,
1082
1083
  toolContext: {
1083
1084
  /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1084
1085
  signin: async () => undefined,
1085
1086
  ...opts?.toolContext,
1087
+ ...owner,
1086
1088
  summarize,
1087
1089
  },
1088
1090
  }),
@@ -1102,7 +1104,6 @@ async function main(agentName, options) {
1102
1104
  runAgentOptions: {
1103
1105
  toolChoiceRequired: (0, commands_1.getToolChoiceRequired)(),
1104
1106
  traceId: (0, nerves_1.createTraceId)(),
1105
- mcpManager,
1106
1107
  toolContext,
1107
1108
  },
1108
1109
  failoverState,
@@ -1157,7 +1157,8 @@ async function runPrivateRuntimeTurn(options) {
1157
1157
  const innerCapabilities = (0, friends_1.getChannelCapabilities)("inner");
1158
1158
  const selfFriend = createSelfFriend(agentName);
1159
1159
  const selfContext = { friend: selfFriend, channel: innerCapabilities };
1160
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
1160
+ const mcpOwner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
1161
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(mcpOwner) ?? undefined;
1161
1162
  const relationshipAwaitCoordinatesValue = parsedAwait ? relationshipAwaitCoordinates(parsedAwait) : null;
1162
1163
  const relationshipAwait = relationshipAwaitCoordinatesValue
1163
1164
  ? await (async () => {
@@ -1301,11 +1302,15 @@ async function runPrivateRuntimeTurn(options) {
1301
1302
  }
1302
1303
  const externalEventExcludedTools = new Set(["rest", "settle", "speak"]);
1303
1304
  const externalEventToolsResolved = options?.externalEvent
1304
- ? (0, tools_1.getSanctuaryRelationshipTools)(externalEventRelationship.relationshipAuthorization.advertisedToolNames)
1305
+ ? (0, tools_1.getToolsForChannel)(innerCapabilities, undefined, undefined, undefined, undefined, undefined, {
1306
+ agentName, relationshipAuthorization: externalEventRelationship.relationshipAuthorization,
1307
+ })
1305
1308
  .filter((tool) => !externalEventExcludedTools.has(tool.function.name))
1306
1309
  : undefined;
1307
1310
  const relationshipAwaitToolsResolved = relationshipAwait
1308
- ? (0, tools_1.getSanctuaryRelationshipTools)(relationshipAwait.relationshipAuthorization.advertisedToolNames)
1311
+ ? (0, tools_1.getToolsForChannel)(innerCapabilities, undefined, undefined, undefined, undefined, undefined, {
1312
+ agentName, relationshipAuthorization: relationshipAwait.relationshipAuthorization,
1313
+ })
1309
1314
  : undefined;
1310
1315
  const effectiveHabitSession = options?.noSend === true
1311
1316
  ? reduceHabitSessionToNoSend(options.habitSession)
@@ -1429,6 +1434,7 @@ async function runPrivateRuntimeTurn(options) {
1429
1434
  } : {}),
1430
1435
  ...(options?.noSend ? { noSend: true } : {}),
1431
1436
  ...(effectiveHabitSession ? { habitSession: effectiveHabitSession } : {}),
1437
+ ...mcpOwner,
1432
1438
  },
1433
1439
  ...(effectiveHabitSession ? { habitSession: effectiveHabitSession } : {}),
1434
1440
  },
@@ -66,9 +66,9 @@ const OUTWARD_DELIVERY_TOOL_ACKS = new Map([
66
66
  ["settle", "(delivered)"],
67
67
  ["speak", "(spoken)"],
68
68
  ]);
69
- async function releaseRuntimeMcpServersAfterTurn() {
69
+ async function releaseRuntimeMcpServersAfterTurn(owner) {
70
70
  const manager = await Promise.resolve().then(() => __importStar(require("../repertoire/mcp-manager")));
71
- await manager.releaseRuntimeMcpServers();
71
+ await manager.releaseRuntimeMcpServers(owner);
72
72
  }
73
73
  /**
74
74
  * Strip MiniMax-style `<think>...</think>` reasoning blocks from a response
@@ -323,19 +323,21 @@ function getSenseSessionPath(agentName, friendId, channel, sessionKey, agentRoot
323
323
  */
324
324
  async function runSenseTurn(options) {
325
325
  return (0, turn_execution_lease_1.withTurnExecutionLease)(async () => {
326
- (0, identity_1.setAgentName)(options.agentName);
326
+ const owner = Object.freeze({ agentName: options.agentName, agentRoot: (0, identity_1.getAgentRoot)(options.agentName) });
327
+ (0, identity_1.setAgentName)(owner.agentName);
327
328
  try {
328
- return await runSenseTurnExclusive(options);
329
+ return await runSenseTurnExclusive(options, owner);
329
330
  }
330
331
  finally {
331
332
  if (options.runtimeMcpServers && !options.disableTools) {
332
- await releaseRuntimeMcpServersAfterTurn();
333
+ await releaseRuntimeMcpServersAfterTurn(owner);
333
334
  }
334
335
  }
335
336
  });
336
337
  }
337
- async function runSenseTurnExclusive(options) {
338
- const { agentName, channel, sessionKey, friendId, userMessage } = options;
338
+ async function runSenseTurnExclusive(options, owner) {
339
+ const { channel, sessionKey, friendId, userMessage } = options;
340
+ const { agentName, agentRoot } = owner;
339
341
  (0, runtime_1.emitNervesEvent)({
340
342
  component: "senses",
341
343
  event: "senses.shared_turn_start",
@@ -343,7 +345,6 @@ async function runSenseTurnExclusive(options) {
343
345
  meta: { agentName, channel, sessionKey, friendId },
344
346
  });
345
347
  // Resolve context
346
- const agentRoot = (0, identity_1.getAgentRoot)(agentName);
347
348
  const friendsPath = path.join(agentRoot, "friends");
348
349
  const friendStore = new friends_1.FileFriendStore(friendsPath);
349
350
  const capabilities = (0, friends_1.getChannelCapabilities)(channel);
@@ -386,7 +387,7 @@ async function runSenseTurnExclusive(options) {
386
387
  // Runtime MCP servers (e.g. Workbench's ouro_workbench) are passed per-turn for THIS agent only.
387
388
  const mcpManager = options.disableTools
388
389
  ? undefined
389
- : await (0, mcp_manager_1.getSharedMcpManager)(options.runtimeMcpServers ? { runtimeServers: options.runtimeMcpServers } : undefined) ?? undefined;
390
+ : await (0, mcp_manager_1.getSharedMcpManager)({ ...owner, runtimeServers: options.runtimeMcpServers }) ?? undefined;
390
391
  // Session path and loading
391
392
  const ephemeralRoot = options.disablePersistence
392
393
  ? fs.mkdtempSync(path.join(os.tmpdir(), "ouro-observe-only-"))
@@ -576,6 +577,8 @@ async function runSenseTurnExclusive(options) {
576
577
  toolContext: {
577
578
  signin: async () => undefined,
578
579
  ...(options.toolContext ? options.toolContext : {}),
580
+ agentName,
581
+ agentRoot,
579
582
  currentUserMessage: userMessage,
580
583
  },
581
584
  },
@@ -643,6 +643,8 @@ function handleTeamsSlashCommand(text, registry, friendId, conversationId, strea
643
643
  // Handle an incoming Teams message
644
644
  async function handleTeamsMessage(text, stream, conversationId, teamsContext, sendMessage, reactionOverrides, runtimeOverrides) {
645
645
  const turnKey = teamsTurnKey(conversationId);
646
+ const agentName = (0, identity_1.getAgentName)();
647
+ const owner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
646
648
  // NOTE: Confirmation resolution is handled in the app.on("message") handler
647
649
  // BEFORE the conversation lock. By the time we get here, any pending
648
650
  // confirmation has already been resolved and the reply consumed.
@@ -668,7 +670,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
668
670
  const traceId = (0, nerves_1.createTraceId)();
669
671
  const sessPath = (0, config_2.sessionPath)(friendId, "teams", conversationId);
670
672
  const teamsCapabilities = (0, friends_1.getChannelCapabilities)("teams");
671
- const pendingDir = (0, pending_1.getPendingDir)((0, identity_1.getAgentName)(), friendId, "teams", conversationId);
673
+ const pendingDir = (0, pending_1.getPendingDir)(agentName, friendId, "teams", conversationId);
672
674
  const runWithLease = runtimeOverrides?._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
673
675
  try {
674
676
  await runWithLease(sessPath, async (sessionTurnLease) => {
@@ -678,17 +680,17 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
678
680
  }
679
681
  await new Promise(r => setImmediate(r));
680
682
  // Build Teams-specific toolContext fields for injection into the pipeline
681
- const teamsToolContext = teamsContext ? {
682
- graphToken: teamsContext.graphToken,
683
- adoToken: teamsContext.adoToken,
684
- githubToken: teamsContext.githubToken,
685
- signin: teamsContext.signin,
686
- summarize: (0, core_1.createSummarize)("human"),
687
- tenantId: teamsContext.tenantId,
688
- botApi: teamsContext.botApi,
689
- } : {};
683
+ const teamsToolContext = { ...owner, ...(teamsContext ? {
684
+ graphToken: teamsContext.graphToken,
685
+ adoToken: teamsContext.adoToken,
686
+ githubToken: teamsContext.githubToken,
687
+ signin: teamsContext.signin,
688
+ summarize: (0, core_1.createSummarize)("human"),
689
+ tenantId: teamsContext.tenantId,
690
+ botApi: teamsContext.botApi,
691
+ } : {}) };
690
692
  let currentText = text;
691
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
693
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(owner) ?? undefined;
692
694
  while (true) {
693
695
  let drainedSteeringFollowUps = [];
694
696
  // Build runAgentOptions with Teams-specific fields
@@ -759,13 +761,14 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
759
761
  hasExistingGroupWithFamily: false,
760
762
  enforceTrustGate: trust_gate_1.enforceTrustGate,
761
763
  drainPending: pending_1.drainPending,
762
- drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)((0, identity_1.getAgentName)(), deferredFriendId),
764
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(agentName, deferredFriendId),
763
765
  runAgent: (msgs, cb, channel, sig, opts) => (0, core_1.runAgent)(msgs, cb, channel, sig, {
764
766
  ...opts,
765
767
  toolContext: {
766
768
  /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
767
769
  signin: async () => undefined,
768
770
  ...opts?.toolContext,
771
+ ...owner,
769
772
  summarize: teamsToolContext.summarize,
770
773
  },
771
774
  }),
@@ -41,6 +41,7 @@ exports.executeApprovedTelegramTool = executeApprovedTelegramTool;
41
41
  exports.createTelegramApprovalRuntime = createTelegramApprovalRuntime;
42
42
  const path = __importStar(require("node:path"));
43
43
  const node_crypto_1 = require("node:crypto");
44
+ const friends_1 = require("@ouro.bot/friends");
44
45
  const approval_files_1 = require("../heart/approval-files");
45
46
  const approval_store_1 = require("../heart/approval-store");
46
47
  const tool_approval_1 = require("../heart/tool-approval");
@@ -52,6 +53,7 @@ const telegram_1 = require("./telegram");
52
53
  const context_1 = require("../mind/context");
53
54
  const session_transaction_1 = require("../mind/session-transaction");
54
55
  const tools_1 = require("../repertoire/tools");
56
+ const mcp_manager_1 = require("../repertoire/mcp-manager");
55
57
  const runtime_1 = require("../nerves/runtime");
56
58
  const telegram_client_1 = require("./telegram-client");
57
59
  function telegramApprovalCommitBarrierHooks(effectBarrier) {
@@ -89,7 +91,17 @@ async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDi
89
91
  });
90
92
  try {
91
93
  effectBarrier();
92
- const result = await execute(name, args);
94
+ const outcome = await execute(name, args);
95
+ if (outcome.kind !== "handler_succeeded") {
96
+ if (name === "unraid_restart_container")
97
+ (0, runtime_1.emitNervesEvent)({
98
+ level: "error", component: "senses", event: "senses.telegram_approved_restart_error",
99
+ message: "approved Sanctuary restart did not complete",
100
+ meta: { ...(scenarioHandleDigest ? { scenarioHandleDigest } : {}), ...(approvalId ? { approvalId } : {}), outcome: outcome.kind },
101
+ });
102
+ return outcome;
103
+ }
104
+ const result = outcome.text;
93
105
  if (name === "sanctuary_resume_download_queue") {
94
106
  let parsed;
95
107
  try {
@@ -99,14 +111,14 @@ async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDi
99
111
  throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume returned an invalid result");
100
112
  }
101
113
  const data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
102
- const outcome = data?.data && typeof data.data === "object" && !Array.isArray(data.data) ? data.data : null;
103
- const after = outcome?.after && typeof outcome.after === "object" && !Array.isArray(outcome.after) ? outcome.after : null;
104
- if (data?.ok !== true || outcome?.verified !== true || after?.paused !== false)
114
+ const verification = data?.data && typeof data.data === "object" && !Array.isArray(data.data) ? data.data : null;
115
+ const after = verification?.after && typeof verification.after === "object" && !Array.isArray(verification.after) ? verification.after : null;
116
+ if (data?.ok !== true || verification?.verified !== true || after?.paused !== false)
105
117
  throw new tool_approval_1.ApprovalExecutionFailedError("approved download resume was not independently verified");
106
- return result;
118
+ return outcome;
107
119
  }
108
120
  if (name !== "unraid_restart_container")
109
- return result;
121
+ return outcome;
110
122
  let parsed;
111
123
  try {
112
124
  parsed = JSON.parse(result);
@@ -147,7 +159,7 @@ async function executeApprovedTelegramTool(name, args, execute, scenarioHandleDi
147
159
  message: "approved Sanctuary restart execution completed",
148
160
  meta: { ...(scenarioHandleDigest ? { scenarioHandleDigest } : {}), ...(approvalId ? { approvalId } : {}), observedRestart: true },
149
161
  });
150
- return result;
162
+ return outcome;
151
163
  }
152
164
  catch (error) {
153
165
  if (name === "unraid_restart_container")
@@ -176,9 +188,42 @@ function createTelegramApprovalRuntime(options) {
176
188
  const acceptanceMarker = options.dependencies?.acceptanceMarker ?? (() => (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(options.agentName));
177
189
  const provider = options.dependencies?.runProvider ?? core_1.runAgent;
178
190
  const resolveTool = options.dependencies?.resolveTool ?? tools_1.resolveToolDefinition;
179
- const executeTool = options.dependencies?.executeTool ?? tools_1.execTool;
180
191
  const agentRoot = options.dependencies?.agentRoot ?? (0, identity_1.getAgentRoot)(options.agentName);
181
- const liveToolContext = { ...options.toolContext, agentRoot: options.toolContext.agentRoot ?? agentRoot };
192
+ const owner = Object.freeze({ agentName: options.agentName, agentRoot });
193
+ const currentOptions = async (record) => {
194
+ try {
195
+ if (!options.resolveLiveToolContext)
196
+ throw new Error("current owner authority producer is unavailable");
197
+ const context = await options.resolveLiveToolContext(record);
198
+ const relationship = context.relationshipAuthorization;
199
+ if (context.agentName !== owner.agentName || context.agentRoot !== owner.agentRoot
200
+ || relationship?.profileId !== "sanctuary-owner" || relationship.actor?.trustLevel !== "family"
201
+ || context.context?.friend.trustLevel !== "family" || context.context.friend.id !== relationship.actor.friendId
202
+ || context.currentSession?.friendId !== relationship.actor.friendId
203
+ || context.currentSession.key !== record.sessionKey || context.currentSession.sessionPath !== record.sessionPath) {
204
+ throw new Error("current approval owner coordinates are not exact");
205
+ }
206
+ const runtime = await (options.dependencies?.getProviderRuntime ?? core_1.getProviderRuntime)("human", owner);
207
+ const mcpManager = await (options.dependencies?.getSharedMcpManager ?? mcp_manager_1.getSharedMcpManager)(owner) ?? undefined;
208
+ const selectCurrentTools = () => (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)("telegram"), context.context?.friend.toolPreferences, context.context, runtime.capabilities, mcpManager, runtime.model, context);
209
+ return {
210
+ providerRuntimeOverride: runtime, mcpManager,
211
+ toolContext: { ...context, toolSelection: selectCurrentTools(), selectCurrentTools },
212
+ };
213
+ }
214
+ catch (error) {
215
+ (0, runtime_1.emitNervesEvent)({
216
+ level: "warn", component: "senses", event: "senses.telegram_approval_authority_unavailable",
217
+ message: "current approval authority could not be reconstructed; execution remains disabled",
218
+ meta: { approvalId: record.approvalId, category: error instanceof Error ? error.name : "unknown" },
219
+ });
220
+ return null;
221
+ }
222
+ };
223
+ const invocationContext = (current, definition) => ({
224
+ ...current.toolContext,
225
+ toolSelection: Object.freeze({ ordinary: Object.freeze([definition]), engine: Object.freeze([]) }),
226
+ });
182
227
  const stateRoot = path.join(agentRoot, "state", "approvals");
183
228
  const store = (0, approval_store_1.openApprovalStore)({ databasePath: path.join(stateRoot, "approvals.sqlite"), now: () => new Date(now()) });
184
229
  const checkpoints = new approval_files_1.FileApprovalCheckpointStore(path.join(stateRoot, "checkpoints.json"));
@@ -297,6 +342,8 @@ function createTelegramApprovalRuntime(options) {
297
342
  const continuationOwnerId = `telegram-continuation-${(0, node_crypto_1.randomUUID)()}`;
298
343
  let continuationEpoch = 0;
299
344
  let continuationCausalEventId;
345
+ let continuationAuthorized = false;
346
+ let controlNotice;
300
347
  const continuationCoordinator = {
301
348
  propose: (request) => coordinator({
302
349
  sessionPath: record.sessionPath,
@@ -321,7 +368,11 @@ function createTelegramApprovalRuntime(options) {
321
368
  markContinuationAttempted: () => { effectBarrier(); store.markContinuationAttempted({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
322
369
  completeContinuation: () => { effectBarrier(); store.completeContinuation({ approvalId: record.approvalId, ownerId: continuationOwnerId, epoch: continuationEpoch }); },
323
370
  runAgent: provider,
324
- runAgentOptions: approvalContinuationRunAgentOptions(liveToolContext, continuationCoordinator),
371
+ revalidate: async () => {
372
+ const current = await currentOptions(record);
373
+ continuationAuthorized = current !== null;
374
+ return current ? { ...current, ...approvalContinuationRunAgentOptions(current.toolContext, continuationCoordinator) } : null;
375
+ },
325
376
  persist: (messages, result) => {
326
377
  effectBarrier();
327
378
  const existingEventIds = new Set((0, session_events_1.loadSessionEnvelopeFile)(record.sessionPath)?.events.map((event) => event.id) ?? []);
@@ -332,6 +383,15 @@ function createTelegramApprovalRuntime(options) {
332
383
  },
333
384
  deliver: async (text) => {
334
385
  effectBarrier();
386
+ if (!continuationAuthorized) {
387
+ controlNotice = text;
388
+ (0, runtime_1.emitNervesEvent)({
389
+ component: "senses", event: "senses.telegram_approval_control_only",
390
+ message: "approval result will use its existing terminal control instead of an unauthorized Butler message",
391
+ meta: { approvalId: record.approvalId, state: record.state },
392
+ });
393
+ return;
394
+ }
335
395
  const messageIds = await options.effects.sendText({ idempotencyKey: `approval:${record.approvalId}:continuation:${(0, node_crypto_1.createHash)("sha256").update(text).digest("hex")}`, chatId: options.authorizedChatId, text, authorClass: "butler", ...(continuationCausalEventId ? { causalEventId: continuationCausalEventId } : {}) });
336
396
  effectBarrier();
337
397
  if (acceptanceBinding) {
@@ -352,7 +412,7 @@ function createTelegramApprovalRuntime(options) {
352
412
  }
353
413
  },
354
414
  });
355
- return terminalOutcome(record);
415
+ return { ...terminalOutcome(record), ...(controlNotice ? { terminalText: controlNotice } : {}) };
356
416
  });
357
417
  };
358
418
  transport = (0, telegram_client_1.createTelegramApprovalTransport)({
@@ -397,6 +457,7 @@ function createTelegramApprovalRuntime(options) {
397
457
  }
398
458
  else if (existing.state === "proposed") {
399
459
  const ownerId = `telegram-decision-${(0, node_crypto_1.randomUUID)()}`;
460
+ let definition;
400
461
  record = await (0, session_transaction_1.withSessionTurnLease)(existing.sessionPath, async (lease) => (0, tool_approval_1.executeApprovalDecision)({
401
462
  approvalStore: store,
402
463
  checkpointStore: checkpoints,
@@ -410,16 +471,31 @@ function createTelegramApprovalRuntime(options) {
410
471
  },
411
472
  ownerId,
412
473
  currentSessionRevision: (0, session_transaction_1.readSessionTransaction)(existing.sessionPath, lease).revision,
413
- resolveTool,
414
- resolveApprovalPolicy: (name, args) => (0, tools_1.approvalPolicyForInvocation)(name, args, liveToolContext),
474
+ resolveTool: async (name) => {
475
+ const current = await currentOptions(existing);
476
+ definition = current ? resolveTool(name, current.toolContext.toolSelection) : undefined;
477
+ return definition;
478
+ },
479
+ resolveApprovalPolicy: async (name, args) => {
480
+ const current = await currentOptions(existing);
481
+ return current ? (0, tools_1.approvalPolicyForInvocation)(name, args, current.toolContext) : { kind: "not_required" };
482
+ },
415
483
  liveGuard: async () => ({ ok: true }),
416
484
  liveRisk: async () => ({ ok: true }),
485
+ preflight: async (context) => {
486
+ const current = await currentOptions(existing);
487
+ if (!current)
488
+ return { ok: false, reason: "current tool authority is unavailable" };
489
+ const result = await (0, tools_1.preflightToolCall)(context.record.toolName, context.arguments, invocationContext(current, context.definition));
490
+ return result.kind === "ready" ? { ok: true } : { ok: false, reason: result.text };
491
+ },
417
492
  hooks: telegramApprovalDecisionBarrierHooks(effectBarrier),
418
- execute: (name, args) => {
419
- const approvedToolContext = name === "unraid_restart_container"
420
- ? { ...options.toolContext, relationshipAuthorization: undefined }
421
- : options.toolContext;
422
- const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => executeTool(toolName, toolArgs, approvedToolContext), decisionScenarioDigest, existing.approvalId, effectBarrier);
493
+ execute: async (name, args) => {
494
+ const current = await currentOptions(existing);
495
+ if (!current || !definition)
496
+ return { kind: "rejected_before_handler", text: "current approved tool authority is unavailable" };
497
+ const approvedToolContext = invocationContext(current, definition);
498
+ const execute = () => executeApprovedTelegramTool(name, args, (toolName, toolArgs) => (0, tools_1.executeTool)(toolName, toolArgs, approvedToolContext, options.dependencies?.executeTool), decisionScenarioDigest, existing.approvalId, effectBarrier);
423
499
  return decisionScenarioDigest
424
500
  ? (0, sanctuary_acceptance_marker_1.runWithSanctuaryAcceptanceApproval)({ approvalId: existing.approvalId, argumentDigest: existing.argumentDigest }, execute)
425
501
  : execute();
@@ -460,7 +536,7 @@ function createTelegramApprovalRuntime(options) {
460
536
  if (!existing) {
461
537
  if (mustFailClosed)
462
538
  throw new Error("Telegram fenced approval journal is unavailable");
463
- const orphanRecovery = await transport.terminalizeOrphaned(pending.approvalId, "⚠️ Approval record is unavailable — no action was taken");
539
+ const orphanRecovery = await transport.terminalizeOrphaned(pending.approvalId, "⚠️ Approval record is unavailable — the action outcome is unknown and will not be retried");
464
540
  (0, runtime_1.emitNervesEvent)({
465
541
  component: "senses",
466
542
  event: "senses.telegram_approval_orphan_recovered",
@@ -562,5 +638,38 @@ function createTelegramApprovalRuntime(options) {
562
638
  subject: options.subject,
563
639
  });
564
640
  };
565
- return { transport, coordinator, legacySubjects, migrateIdentity, recover, close: () => store.close() };
641
+ const isPendingTerminalControl = async (input) => {
642
+ if (input.authorClass !== "control")
643
+ return false;
644
+ const effect = input.effect;
645
+ for (const pending of transport.listPendingDeliveries()) {
646
+ const record = store.read(pending.approvalId);
647
+ if (!record || !pending.messageId || record.transport !== "telegram"
648
+ || record.requesterId !== options.subject || record.transportUserId !== options.subject
649
+ || record.transportChatId !== options.subject || record.sessionKey !== `telegram:${options.subject}`
650
+ || record.transportMessageId !== opaqueTelegramMessageBinding(options.subject, pending.messageId)
651
+ || !["succeeded", "failed", "attempted_indeterminate", "denied", "expired", "drifted", "session_head_changed", "abandoned_before_attempt"].includes(record.state)
652
+ || (pending.terminal && pending.terminal.accepted !== (record.state === "succeeded")))
653
+ continue;
654
+ let matches = false;
655
+ if (effect.kind === "callback_ack") {
656
+ const digest = (0, node_crypto_1.createHash)("sha256").update(effect.callbackQueryId).digest("hex");
657
+ matches = Boolean(pending.terminal && effect.text === undefined && effect.showAlert !== true
658
+ && pending.decisionAttempt?.queryIdDigest === digest && input.idempotencyKey === `approval-callback:${digest}`);
659
+ }
660
+ else if (effect.kind === "edit") {
661
+ const terminalText = pending.terminal?.terminalText
662
+ ?? (record.state === "expired" && pending.expiryObservation?.deadlineAt === pending.expiresAt
663
+ && pending.expiryObservation.observedAt >= pending.expiresAt ? telegram_client_1.TELEGRAM_APPROVAL_EXPIRED_TEXT : undefined);
664
+ matches = terminalText !== undefined && effect.text === terminalText && effect.messageId === Number(pending.messageId)
665
+ && input.idempotencyKey === `approval:${pending.approvalId}:edit:${(0, node_crypto_1.createHash)("sha256").update(terminalText).digest("hex")}`;
666
+ }
667
+ if (!matches)
668
+ continue;
669
+ await transport.validatePendingTerminalControl(pending.approvalId);
670
+ return true;
671
+ }
672
+ return false;
673
+ };
674
+ return { transport, coordinator, legacySubjects, migrateIdentity, recover, isPendingTerminalControl, close: () => store.close() };
566
675
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = exports.TELEGRAM_APPROVAL_TTL_MS = exports.FileTelegramPendingApprovalStore = exports.FileTelegramUpdateInboxStore = exports.FileTelegramOffsetStore = exports.TelegramApiError = void 0;
3
+ exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = exports.TELEGRAM_APPROVAL_EXPIRED_TEXT = exports.TELEGRAM_APPROVAL_TTL_MS = exports.FileTelegramPendingApprovalStore = exports.FileTelegramUpdateInboxStore = exports.FileTelegramOffsetStore = exports.TelegramApiError = void 0;
4
4
  exports.createTelegramLongPoll = createTelegramLongPoll;
5
5
  exports.classifyTelegramPersistedApprovalState = classifyTelegramPersistedApprovalState;
6
6
  exports.createTelegramApprovalTransport = createTelegramApprovalTransport;
@@ -788,6 +788,7 @@ class FileTelegramPendingApprovalStore {
788
788
  }
789
789
  exports.FileTelegramPendingApprovalStore = FileTelegramPendingApprovalStore;
790
790
  exports.TELEGRAM_APPROVAL_TTL_MS = 300_000;
791
+ exports.TELEGRAM_APPROVAL_EXPIRED_TEXT = "⚠️ Approval expired";
791
792
  exports.TELEGRAM_APPROVAL_TERMINAL_EDIT_TIMEOUT_MS = 30_000;
792
793
  exports.TELEGRAM_APPROVAL_TOMBSTONE_TTL_MS = 600_000;
793
794
  function assertTelegramCallbackData(value) {
@@ -1208,7 +1209,7 @@ function createTelegramApprovalTransport(options) {
1208
1209
  }
1209
1210
  const observation = ensureExpiryObservation(current);
1210
1211
  await options.onExpire?.(current.approvalId);
1211
- const terminalizedAt = await editTerminal(current, "⚠️ Approval expired", observation.observedAt);
1212
+ const terminalizedAt = await editTerminal(current, exports.TELEGRAM_APPROVAL_EXPIRED_TEXT, observation.observedAt);
1212
1213
  if (current.acceptanceBinding)
1213
1214
  persistMutation(current, () => retainTerminalTombstone(current, terminalizedAt));
1214
1215
  else {
@@ -1225,6 +1226,22 @@ function createTelegramApprovalTransport(options) {
1225
1226
  throw firstFailure;
1226
1227
  };
1227
1228
  return {
1229
+ async validatePendingTerminalControl(approvalId) {
1230
+ const pending = uniquePending().find((entry) => entry.approvalId === approvalId);
1231
+ if (!pending || !options.signAcceptanceEvidence)
1232
+ throw new Error("Telegram terminal control integrity is unavailable");
1233
+ const state = classifyTelegramPersistedApprovalState(pending);
1234
+ if (state === "action_terminal") {
1235
+ await validateDecisionAttempt(pending);
1236
+ validateTerminalOutcome(pending);
1237
+ }
1238
+ else if (state === "expiry_observed") {
1239
+ validateExpiryObservation(pending);
1240
+ }
1241
+ else {
1242
+ throw new Error("Telegram pending terminal control is not authenticated");
1243
+ }
1244
+ },
1228
1245
  async sendApproval(input) {
1229
1246
  effectBarrier();
1230
1247
  const approveCallbackData = `a:${options.createOpaqueHandle()}`;
@@ -846,6 +846,7 @@ function createTelegramSenseApp(options) {
846
846
  throw primaryError;
847
847
  };
848
848
  let effectJournal;
849
+ let approvalRuntime;
849
850
  const getEffectJournal = () => {
850
851
  effectJournal ??= new telegram_effect_adapter_1.FileTelegramEffectJournal(path.join(agentRoot, "state", "telegram", "effects"));
851
852
  return effectJournal;
@@ -867,6 +868,13 @@ function createTelegramSenseApp(options) {
867
868
  if (input.authorClass === "system_failsafe" && (input.effect.kind !== "text" || input.effect.text !== telegram_effect_adapter_1.FIXED_USENET_SYSTEM_FAILSAFE || !input.idempotencyKey.startsWith("system-failsafe:"))) {
868
869
  return { allowed: false, reason: "system failsafe shape is not fixed" };
869
870
  }
871
+ if (target.friendId === configuredOwnerFriendId && target.sessionKey === configuredOwnerSessionKey
872
+ && await approvalRuntime?.isPendingTerminalControl?.(input)) {
873
+ return {
874
+ allowed: true, receiptId: `approval-terminal:${(0, node_crypto_1.createHash)("sha256").update(input.idempotencyKey).digest("hex")}`,
875
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), transport: { chatId: authorizedChatId },
876
+ };
877
+ }
870
878
  if (options.authorizeRelationshipEffect) {
871
879
  if (target.friendId === configuredOwnerFriendId && target.sessionKey !== configuredOwnerSessionKey)
872
880
  return { allowed: false, reason: "owner relationship session binding changed" };
@@ -891,7 +899,6 @@ function createTelegramSenseApp(options) {
891
899
  };
892
900
  const approvalEffects = (0, telegram_effect_adapter_1.createTelegramApprovalEffectPort)({ target: configuredOwnerTarget(), chatId: authorizedChatId, execute: executeAuthorizedEffect, record: recordConfiguredOwnerEffect });
893
901
  let toolContext;
894
- let approvalRuntime;
895
902
  let approvalTransport;
896
903
  let interactiveControl;
897
904
  try {
@@ -906,9 +913,38 @@ function createTelegramSenseApp(options) {
906
913
  subject,
907
914
  identityKey,
908
915
  toolContext: toolContext ?? {},
916
+ resolveLiveToolContext: async (record) => {
917
+ const sessionPath = (0, shared_turn_1.getSenseSessionPath)(options.agentName, configuredOwnerFriendId, "telegram", configuredOwnerSessionKey, agentRoot);
918
+ if (record.transport !== "telegram" || record.requesterId !== subject
919
+ || record.transportUserId !== subject || record.transportChatId !== subject
920
+ || record.sessionKey !== configuredOwnerSessionKey || path.resolve(record.sessionPath) !== path.resolve(sessionPath)) {
921
+ throw new Error("approval is not bound to the configured owner session");
922
+ }
923
+ const envelope = (0, session_events_1.loadSessionEnvelopeFile)(sessionPath);
924
+ const ingress = envelope && (0, session_events_1.selectEffectiveSessionEvents)(envelope.events).findLast((event) => event.role === "user");
925
+ if (!ingress)
926
+ throw new Error("approval owner ingress is unavailable");
927
+ const relationshipAuthorization = await resolveLiveRelationshipAuthorization({
928
+ friendId: configuredOwnerFriendId, requestId: `approval:${record.approvalId}`, sessionEventId: ingress.id,
929
+ botId: botId, userId: authorizedUserId, chatId: authorizedChatId, sessionKey: configuredOwnerSessionKey,
930
+ });
931
+ if (relationshipAuthorization.profileId !== "sanctuary-owner")
932
+ throw new Error("approval owner profile is unavailable");
933
+ const friendStore = new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
934
+ const friend = await friendStore.get(configuredOwnerFriendId);
935
+ if (!friend)
936
+ throw new Error("approval owner Friend is unavailable");
937
+ return {
938
+ ...toolContext, signin: async () => undefined, agentName: options.agentName, agentRoot,
939
+ currentSession: { friendId: friend.id, channel: "telegram", key: configuredOwnerSessionKey, sessionPath },
940
+ currentUserMessage: (0, session_events_1.extractEventText)(ingress),
941
+ context: { friend, channel: (0, friends_1.getChannelCapabilities)("telegram") }, friendStore, relationshipAuthorization,
942
+ };
943
+ },
909
944
  effects: approvalEffects,
910
945
  effectBarrier: acceptanceAuditBarrier,
911
946
  dependencies: {
947
+ agentRoot,
912
948
  acceptanceMarker: () => {
913
949
  const scenarioHandleDigest = readScenarioHandleDigest();
914
950
  return scenarioHandleDigest ? { scenarioHandleDigest } : null;
@@ -1075,25 +1111,28 @@ function createTelegramSenseApp(options) {
1075
1111
  (0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.telegram_system_failsafe_error", message: "Telegram system failsafe reconciliation failed", meta: { agentName: options.agentName, subject, error: transportError(error) } });
1076
1112
  }
1077
1113
  };
1114
+ async function resolveLiveRelationshipAuthorization(input) {
1115
+ if (!options.resolveRelationshipAuthorization)
1116
+ throw new Error("Telegram relationship authorization resolver is unavailable");
1117
+ const authorization = await options.resolveRelationshipAuthorization(input);
1118
+ if (authorization.subject.friendId !== input.friendId || authorization.subject.admissionState !== "active")
1119
+ throw new Error("Telegram relationship admission is not active");
1120
+ return {
1121
+ requestId: input.requestId,
1122
+ profileId: authorization.profileId,
1123
+ authorizedContextScopes: authorization.authorizedContextScopes,
1124
+ advertisedToolNames: authorization.advertisedToolNames,
1125
+ actor: authorization.actor,
1126
+ resolveCurrent: () => resolveLiveRelationshipAuthorization(input),
1127
+ authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(input)).authorizeTool(name, args),
1128
+ };
1129
+ }
1078
1130
  const prepareRelationshipRunAgentOptions = (input) => {
1079
1131
  if (!options.resolveRelationshipAuthorization)
1080
1132
  throw new Error("Telegram relationship authorization resolver is unavailable");
1081
1133
  const relationshipCoordinates = { ...input, botId: botId };
1082
- const resolveLiveRelationshipAuthorization = async () => {
1083
- const authorization = await options.resolveRelationshipAuthorization(relationshipCoordinates);
1084
- if (authorization.subject.friendId !== input.friendId || authorization.subject.admissionState !== "active")
1085
- throw new Error("Telegram relationship admission is not active");
1086
- return {
1087
- requestId: input.requestId,
1088
- profileId: authorization.profileId,
1089
- authorizedContextScopes: authorization.authorizedContextScopes,
1090
- advertisedToolNames: authorization.advertisedToolNames,
1091
- actor: authorization.actor,
1092
- authorizeTool: async (name, args) => (await options.resolveRelationshipAuthorization(relationshipCoordinates)).authorizeTool(name, args),
1093
- };
1094
- };
1095
1134
  return async ({ runAgentOptions, activeCares = [], careEvidenceNow = Date.now() }) => {
1096
- const relationshipAuthorization = await resolveLiveRelationshipAuthorization();
1135
+ const relationshipAuthorization = await resolveLiveRelationshipAuthorization(relationshipCoordinates);
1097
1136
  const isSanctuaryOwner = options.agentName === "sanctuary" && relationshipAuthorization.profileId === "sanctuary-owner";
1098
1137
  const isSanctuaryHouseholdConversation = options.agentName === "sanctuary"
1099
1138
  && (relationshipAuthorization.profileId === "sanctuary-owner" || relationshipAuthorization.profileId === "sanctuary-household");