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

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 (40) hide show
  1. package/changelog.json +18 -0
  2. package/deploy/unraid/README.txt +3 -6
  3. package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
  4. package/deploy/unraid/sanctuary-deployment-target.mjs +1 -0
  5. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
  6. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  7. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
  8. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  9. package/deploy/unraid/sanctuary.xml +1 -1
  10. package/dist/heart/config.js +32 -2
  11. package/dist/heart/core.js +167 -115
  12. package/dist/heart/daemon/daemon.js +19 -9
  13. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
  14. package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
  15. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
  16. package/dist/heart/frontend-approval-runtime.js +87 -13
  17. package/dist/heart/identity.js +17 -5
  18. package/dist/heart/session-events.js +8 -1
  19. package/dist/heart/tool-approval.js +64 -31
  20. package/dist/mind/pending.js +4 -4
  21. package/dist/repertoire/mcp-manager.js +365 -381
  22. package/dist/repertoire/mcp-tools.js +66 -25
  23. package/dist/repertoire/plugin-mcp.js +3 -3
  24. package/dist/repertoire/shell-sessions.js +8 -7
  25. package/dist/repertoire/tool-arguments.js +30 -8
  26. package/dist/repertoire/tools-session.js +30 -5
  27. package/dist/repertoire/tools-shell.js +30 -14
  28. package/dist/repertoire/tools-voice.js +6 -6
  29. package/dist/repertoire/tools.js +231 -205
  30. package/dist/senses/bluebubbles/index.js +4 -2
  31. package/dist/senses/cli.js +6 -5
  32. package/dist/senses/private-runtime.js +9 -3
  33. package/dist/senses/shared-turn.js +12 -9
  34. package/dist/senses/teams.js +15 -12
  35. package/dist/senses/telegram-approval-runtime.js +129 -20
  36. package/dist/senses/telegram-client.js +19 -2
  37. package/dist/senses/telegram.js +54 -15
  38. package/dist/senses/voice/twilio-phone.js +123 -173
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.surfaceToolDef = exports.speakTool = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
4
- exports.resetMcpDefinitions = resetMcpDefinitions;
5
- exports.getSanctuaryRelationshipTools = getSanctuaryRelationshipTools;
3
+ exports.ToolSelectionError = exports.surfaceToolDef = exports.speakTool = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = exports.SANCTUARY_OWNER_ADDITIONS = void 0;
4
+ exports.toolSelectionSchemas = toolSelectionSchemas;
5
+ exports.selectToolsForChannel = selectToolsForChannel;
6
6
  exports.getToolsForChannel = getToolsForChannel;
7
+ exports.reduceToolSelection = reduceToolSelection;
7
8
  exports.resolveToolDefinition = resolveToolDefinition;
8
9
  exports.approvalPolicyForToolName = approvalPolicyForToolName;
9
10
  exports.classifyApprovalForInvocation = classifyApprovalForInvocation;
@@ -11,9 +12,12 @@ exports.approvalPolicyForInvocation = approvalPolicyForInvocation;
11
12
  exports.shellRiskProfile = shellRiskProfile;
12
13
  exports.riskProfileForTool = riskProfileForTool;
13
14
  exports.riskProfileForToolName = riskProfileForToolName;
15
+ exports.preflightToolCall = preflightToolCall;
16
+ exports.executeTool = executeTool;
14
17
  exports.execTool = execTool;
15
18
  exports.summarizeArgs = summarizeArgs;
16
19
  exports.buildToolResultSummary = buildToolResultSummary;
20
+ const node_util_1 = require("node:util");
17
21
  const tools_base_1 = require("./tools-base");
18
22
  const tools_teams_1 = require("./tools-teams");
19
23
  const tools_bluebubbles_1 = require("./tools-bluebubbles");
@@ -26,12 +30,14 @@ const identity_1 = require("../heart/identity");
26
30
  const store_1 = require("../commerce/store");
27
31
  const tools_surface_1 = require("./tools-surface");
28
32
  const mcp_tools_1 = require("./mcp-tools");
33
+ const tool_arguments_1 = require("./tool-arguments");
29
34
  const tools_voice_1 = require("./tools-voice");
30
35
  const shell_sessions_1 = require("./shell-sessions");
31
36
  const tools_unraid_1 = require("./tools-unraid");
32
37
  const tools_flow_1 = require("./tools-flow");
33
38
  const tools_steward_policy_1 = require("./tools-steward-policy");
34
39
  const steward_policy_1 = require("../heart/steward-policy");
40
+ const tool_approval_1 = require("../heart/tool-approval");
35
41
  function safeGetAgentRoot() {
36
42
  try {
37
43
  return (0, identity_1.getAgentRoot)();
@@ -40,14 +46,6 @@ function safeGetAgentRoot() {
40
46
  return undefined;
41
47
  }
42
48
  }
43
- function isSanctuaryAgent() {
44
- try {
45
- return (0, identity_1.getAgentName)() === "sanctuary";
46
- }
47
- catch {
48
- return false;
49
- }
50
- }
51
49
  const SANCTUARY_RELATIONSHIP_BASE_TOOLS = new Set([
52
50
  "external_event_disposition",
53
51
  "query_active_work",
@@ -63,23 +61,10 @@ const SANCTUARY_RELATIONSHIP_BASE_TOOLS = new Set([
63
61
  "describe_image",
64
62
  "send_message",
65
63
  ]);
66
- const SANCTUARY_TELEGRAM_SURFACE = new Set([
67
- "query_active_work",
68
- "save_friend_note",
69
- "telegram_contact_manage",
70
- "query_cares",
71
- "care_manage",
72
- "await_condition",
73
- "resolve_await",
74
- "cancel_await",
75
- "list_recent_attachments",
76
- "materialize_attachment",
77
- "describe_image",
78
- ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool.function.name),
79
- "steward_policy_manage",
80
- "ponder",
81
- "settle",
82
- "speak",
64
+ exports.SANCTUARY_OWNER_ADDITIONS = new Set([
65
+ "shell", "shell_status", "shell_tail", "read_file", "write_file", "edit_file", "glob", "grep",
66
+ "web_search", "search_facts", "consult_diary", "consult_notes", "get_friend_note",
67
+ "session_summary", "query_session", "set_reasoning_effort", "restart_runtime", "revive_sense",
83
68
  ]);
84
69
  // Re-export types and constants used by the rest of the codebase
85
70
  var tools_base_2 = require("./tools-base");
@@ -93,44 +78,26 @@ Object.defineProperty(exports, "speakTool", { enumerable: true, get: function ()
93
78
  var tools_surface_2 = require("./tools-surface");
94
79
  Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: function () { return tools_surface_2.surfaceToolDef; } });
95
80
  // All tool definitions in a single registry
96
- const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, tools_surface_1.surfaceToolDefinition];
81
+ const additionalDefinitions = [...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, ...tools_bundle_1.bundleToolDefinitions, ...tools_voice_1.voiceToolDefinitions, ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, tools_surface_1.surfaceToolDefinition];
97
82
  const COMMERCE_AUTHORITY_TOOLS = new Set(["stripe_create_card", "flight_hold", "flight_book"]);
98
- // MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
99
- // Kept separate from allDefinitions so execTool can find them.
100
- let mcpDefinitions = [];
101
- /** Exported for testing — reset the MCP definitions cache. */
102
- function resetMcpDefinitions() {
103
- mcpDefinitions = [];
104
- }
105
- function baseToolsForCapabilities() {
106
- // Use baseToolDefinitions at call time so dynamically-added tools are included
107
- return tools_base_1.baseToolDefinitions.map((d) => d.tool);
83
+ class ToolSelectionError extends Error {
108
84
  }
109
- function uniqueToolSchemas(tools) {
85
+ exports.ToolSelectionError = ToolSelectionError;
86
+ function assertUniqueToolSchemas(tools) {
110
87
  const seen = new Set();
111
- return tools.filter((tool) => {
112
- if (seen.has(tool.function.name))
113
- return false;
88
+ for (const tool of tools) {
89
+ if (seen.has(tool.function.name)) {
90
+ (0, runtime_1.emitNervesEvent)({
91
+ level: "error", component: "tools", event: "tool.selection_collision",
92
+ message: "tool selection contains an ambiguous name", meta: { name: tool.function.name },
93
+ });
94
+ throw new ToolSelectionError(`tool selection collision: ${tool.function.name}`);
95
+ }
114
96
  seen.add(tool.function.name);
115
- return true;
116
- });
97
+ }
117
98
  }
118
- /**
119
- * Resolve Sanctuary relationship tools from one canonical definition pool.
120
- * Durable relationship profiles can only reduce this surface; they cannot
121
- * introduce an arbitrary base tool by naming it in configuration.
122
- */
123
- function getSanctuaryRelationshipTools(advertisedToolNames) {
124
- const advertised = new Set(advertisedToolNames);
125
- return uniqueToolSchemas([
126
- ...tools_base_1.baseToolDefinitions.filter((definition) => SANCTUARY_RELATIONSHIP_BASE_TOOLS.has(definition.tool.function.name)).map((definition) => definition.tool),
127
- ...tools_unraid_1.unraidToolDefinitions.map((definition) => definition.tool),
128
- tools_steward_policy_1.stewardPolicyToolDefinition.tool,
129
- tools_flow_1.ponderTool,
130
- tools_flow_1.restTool,
131
- tools_flow_1.settleTool,
132
- tools_flow_1.speakTool,
133
- ]).filter((tool) => advertised.has(tool.function.name));
99
+ function toolSelectionSchemas(selection) {
100
+ return [...selection.ordinary.map((definition) => definition.tool), ...selection.engine];
134
101
  }
135
102
  // Apply a single tool preference to a tool schema, returning a new object.
136
103
  function applyPreference(tool, pref) {
@@ -142,78 +109,96 @@ function applyPreference(tool, pref) {
142
109
  },
143
110
  };
144
111
  }
145
- // Filter out tools whose requiredCapability is not in the provider's capability set.
146
- // Uses baseToolDefinitions at call time so dynamically-added tools are included.
147
- // Only base tools can have requiredCapability (integration tools do not).
148
- function filterByCapability(toolList, providerCapabilities) {
149
- return toolList.filter((tool) => {
150
- const def = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === tool.function.name);
151
- if (!def?.requiredCapability)
152
- return true;
153
- return providerCapabilities?.has(def.requiredCapability) === true;
154
- });
155
- }
156
- // Return the appropriate tools list based on channel capabilities.
157
- // Base tools (no integration) are always included.
158
- // Teams/integration tools are included only if their integration is in availableIntegrations.
159
- // When toolPreferences is provided, matching preferences are appended to tool descriptions.
160
- // When providerCapabilities is provided, tools with requiredCapability are filtered.
161
- function getToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager, _chatModel) {
162
- if (capabilities?.channel === "telegram" && isSanctuaryAgent()) {
163
- return getSanctuaryRelationshipTools([...SANCTUARY_TELEGRAM_SURFACE]);
164
- }
165
- const baseTools = baseToolsForCapabilities();
166
- const bluebubblesTools = capabilities?.channel === "bluebubbles"
167
- ? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
168
- : [];
169
- const voiceTools = capabilities?.channel === "voice"
170
- ? tools_voice_1.voiceToolDefinitions.map((d) => d.tool)
171
- : [];
172
- let result;
173
- if (!capabilities || capabilities.availableIntegrations.length === 0) {
174
- result = [...baseTools, ...bluebubblesTools, ...voiceTools];
112
+ function selectToolsForChannel(capabilities, toolPreferences, _context, providerCapabilities, mcpManager, _chatModel, context) {
113
+ const relationship = context?.relationshipAuthorization;
114
+ const profileId = relationship?.profileId;
115
+ const sanctuary = (context?.agentName === "sanctuary" && (relationship !== undefined || capabilities?.channel === "telegram"))
116
+ || profileId?.startsWith("sanctuary-") === true;
117
+ const owner = profileId === "sanctuary-owner";
118
+ const knownSanctuaryProfile = relationship !== undefined && (owner || profileId === "sanctuary-household" || profileId === "sanctuary-event");
119
+ const mcp = mcpManager ? (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpManager) : [];
120
+ const native = [...tools_base_1.baseToolDefinitions, ...additionalDefinitions];
121
+ assertUniqueToolSchemas([
122
+ ...native.map((definition) => definition.tool), ...mcp.map((definition) => definition.tool),
123
+ tools_flow_1.ponderTool, tools_flow_1.restTool, tools_flow_1.observeTool, tools_flow_1.settleTool, tools_flow_1.speakTool,
124
+ ]);
125
+ let ordinary;
126
+ if (sanctuary) {
127
+ ordinary = knownSanctuaryProfile
128
+ ? [
129
+ ...tools_base_1.baseToolDefinitions.filter((definition) => SANCTUARY_RELATIONSHIP_BASE_TOOLS.has(definition.tool.function.name)
130
+ || (owner && exports.SANCTUARY_OWNER_ADDITIONS.has(definition.tool.function.name))),
131
+ ...tools_unraid_1.unraidToolDefinitions, tools_steward_policy_1.stewardPolicyToolDefinition, ...(owner ? mcp : []),
132
+ ]
133
+ : [];
175
134
  }
176
135
  else {
177
- const available = new Set(capabilities.availableIntegrations);
178
- const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
179
- // Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
180
- const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
181
- if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
182
- result = [...baseTools, ...bluebubblesTools, ...voiceTools, ...integrationDefs.map((d) => d.tool)];
136
+ const available = new Set(capabilities?.availableIntegrations ?? []);
137
+ const integrations = available.size === 0 ? [] : [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions]
138
+ .filter((definition) => definition.integration ? available.has(definition.integration) : capabilities?.channel === "teams")
139
+ .map((definition) => {
140
+ const preference = definition.integration ? toolPreferences?.[definition.integration] : undefined;
141
+ return preference ? { ...definition, tool: applyPreference(definition.tool, preference) } : definition;
142
+ });
143
+ ordinary = [
144
+ ...tools_base_1.baseToolDefinitions,
145
+ ...(capabilities?.channel === "bluebubbles" ? tools_bluebubbles_1.bluebubblesToolDefinitions : []),
146
+ ...(capabilities?.channel === "voice" ? tools_voice_1.voiceToolDefinitions : []),
147
+ ...integrations, ...mcp,
148
+ ];
149
+ }
150
+ const inner = capabilities?.channel === "inner";
151
+ const habit = context?.habitSession;
152
+ if (inner) {
153
+ const canSend = habit?.toolPolicy.outwardMessagingAllowed === true && habit.toolPolicy.grantedTools.includes("send_message");
154
+ ordinary = ordinary.filter((definition) => canSend || definition.tool.function.name !== "send_message");
155
+ if (!habit || (habit.toolPolicy.outwardMessagingAllowed && habit.toolPolicy.grantedTools.includes("surface"))) {
156
+ ordinary.push(tools_surface_1.surfaceToolDefinition);
183
157
  }
184
- else {
185
- // Build a map of integration -> preference text for fast lookup
186
- const prefMap = new Map();
187
- for (const [key, value] of Object.entries(toolPreferences)) {
188
- prefMap.set(key, value);
189
- }
190
- // Apply preferences to matching integration tools (new objects, no mutation)
191
- // d.integration is guaranteed truthy -- integrationDefs are pre-filtered above
192
- const enrichedIntegrationTools = integrationDefs.map((d) => {
193
- const pref = prefMap.get(d.integration);
194
- return pref ? applyPreference(d.tool, pref) : d.tool;
158
+ }
159
+ ordinary = ordinary.filter((definition) => (!relationship || relationship.advertisedToolNames.includes(definition.tool.function.name))
160
+ && (!definition.requiredCapability || providerCapabilities?.has(definition.requiredCapability) === true));
161
+ const engine = [
162
+ ...(context?.noSend ? [] : [tools_flow_1.ponderTool]),
163
+ ...(inner ? [tools_flow_1.restTool] : [tools_flow_1.observeTool, tools_flow_1.settleTool]),
164
+ ...(capabilities?.chatStyle ? [tools_flow_1.speakTool] : []),
165
+ ].filter((tool) => (!sanctuary || (knownSanctuaryProfile && tool.function.name !== "observe"))
166
+ && (!relationship || relationship.advertisedToolNames.includes(tool.function.name)));
167
+ return Object.freeze({
168
+ ordinary: Object.freeze(ordinary.map((definition) => Object.freeze({
169
+ ...definition,
170
+ tool: (0, tool_arguments_1.freezeToolValue)(structuredClone(definition.tool)),
171
+ ...(definition.riskProfile && typeof definition.riskProfile !== "function"
172
+ ? { riskProfile: (0, tool_arguments_1.freezeToolValue)(structuredClone(definition.riskProfile)) } : {}),
173
+ ...(definition.terminalProjection ? { terminalProjection: (0, tool_arguments_1.freezeToolValue)(structuredClone(definition.terminalProjection)) } : {}),
174
+ }))),
175
+ engine: Object.freeze(engine.map((tool) => (0, tool_arguments_1.freezeToolValue)(tool))),
176
+ });
177
+ }
178
+ function getToolsForChannel(...args) {
179
+ return toolSelectionSchemas(selectToolsForChannel(...args));
180
+ }
181
+ function reduceToolSelection(selection, requested) {
182
+ assertUniqueToolSchemas(requested);
183
+ const canonical = toolSelectionSchemas(selection);
184
+ for (const tool of requested) {
185
+ const match = canonical.find((candidate) => candidate.function.name === tool.function.name);
186
+ if (!match || !(0, node_util_1.isDeepStrictEqual)(match, tool)) {
187
+ (0, runtime_1.emitNervesEvent)({
188
+ level: "warn", component: "tools", event: "tool.selection_rejected",
189
+ message: "tool override is not an exact canonical reduction", meta: { name: tool.function.name },
195
190
  });
196
- result = [...baseTools, ...bluebubblesTools, ...voiceTools, ...enrichedIntegrationTools];
191
+ throw new ToolSelectionError(`tool selection is not a canonical reduction: ${tool.function.name}`);
197
192
  }
198
193
  }
199
- // Append first-class MCP tools when mcpManager is provided
200
- if (mcpManager) {
201
- mcpDefinitions = (0, mcp_tools_1.mcpToolsAsDefinitions)(mcpManager);
202
- const mcpSchemas = mcpDefinitions.map((d) => d.tool);
203
- result = [...result, ...mcpSchemas];
204
- }
205
- return filterByCapability(result, providerCapabilities);
194
+ const names = new Set(requested.map((tool) => tool.function.name));
195
+ return Object.freeze({ ordinary: Object.freeze(selection.ordinary.filter((definition) => names.has(definition.tool.function.name))), engine: selection.engine });
206
196
  }
207
- // Look up a tool definition from the live combined registry (native + MCP).
208
- // The base registry is intentionally consulted at call time so tests and
209
- // runtime extensions cannot leave execution metadata behind a stale snapshot.
210
- function resolveToolDefinition(toolName) {
211
- return tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === toolName)
212
- ?? allDefinitions.find((d) => d.tool.function.name === toolName)
213
- ?? mcpDefinitions.find((d) => d.tool.function.name === toolName);
197
+ function resolveToolDefinition(toolName, selection) {
198
+ return (selection?.ordinary ?? [...tools_base_1.baseToolDefinitions, ...additionalDefinitions]).find((definition) => definition.tool.function.name === toolName);
214
199
  }
215
- function approvalPolicyForToolName(name, args) {
216
- return resolveToolDefinition(name)?.approvalPolicy?.(args) ?? { kind: "not_required" };
200
+ function approvalPolicyForToolName(name, args, selection) {
201
+ return resolveToolDefinition(name, selection)?.approvalPolicy?.(args) ?? { kind: "not_required" };
217
202
  }
218
203
  async function routineActionInvocation(name, args, ctx) {
219
204
  const target = typeof args.container === "string" ? args.container : "";
@@ -236,7 +221,7 @@ async function routineActionInvocation(name, args, ctx) {
236
221
  return decision.allowed ? { key, target, expectedPolicyVersion: decision.policyVersion } : null;
237
222
  }
238
223
  async function classifyApprovalForInvocation(name, args, ctx) {
239
- const fallback = approvalPolicyForToolName(name, args);
224
+ const fallback = approvalPolicyForToolName(name, args, ctx?.toolSelection);
240
225
  if (name !== "unraid_restart_container" || fallback.kind !== "required")
241
226
  return { policy: fallback };
242
227
  const routineActionSelection = await routineActionInvocation(name, args, ctx);
@@ -246,9 +231,6 @@ async function approvalPolicyForInvocation(name, args, ctx) {
246
231
  return (await classifyApprovalForInvocation(name, args, ctx)).policy;
247
232
  }
248
233
  const findDefinition = resolveToolDefinition;
249
- function normalizeGuardArgs(_name, args) {
250
- return args;
251
- }
252
234
  const READ_ONLY_OURO_SHELL_COMMANDS = new Set([
253
235
  "help",
254
236
  "-h",
@@ -321,8 +303,8 @@ function riskProfileForTool(def, name, args) {
321
303
  return def.riskProfile(args);
322
304
  return def.riskProfile ?? { mutates: "none", risk: "low" };
323
305
  }
324
- function riskProfileForToolName(name, args) {
325
- const def = findDefinition(name);
306
+ function riskProfileForToolName(name, args, selection) {
307
+ const def = findDefinition(name, selection);
326
308
  return def ? riskProfileForTool(def, name, args) : null;
327
309
  }
328
310
  function orientationHoldMessage(name, profile, reason) {
@@ -337,73 +319,105 @@ function orientationPolicyBlocks(profile, blockedMutationKinds) {
337
319
  return false;
338
320
  return mutationKindsFor(profile).some((kind) => blockedMutationKinds.includes(kind));
339
321
  }
340
- async function execTool(name, args, ctx) {
341
- (0, runtime_1.emitNervesEvent)({
342
- event: "tool.start",
343
- component: "tools",
344
- message: "tool execution started",
345
- meta: { name, ...(name === "shell" && args.command ? { command: args.command } : {}) },
346
- });
347
- // Look up from combined registry (native + MCP)
348
- const def = findDefinition(name);
349
- if (!def) {
350
- (0, runtime_1.emitNervesEvent)({
351
- level: "error",
352
- event: "tool.error",
353
- component: "tools",
354
- message: "unknown tool requested",
355
- meta: { name },
356
- });
357
- return `unknown: ${name}`;
358
- }
359
- const relationshipDecision = await ctx?.relationshipAuthorization?.authorizeTool(name, args);
360
- if (relationshipDecision && !relationshipDecision.allowed) {
361
- (0, runtime_1.emitNervesEvent)({
362
- level: "warn",
363
- event: "tool.relationship_authorization_block",
364
- component: "tools",
365
- message: "relationship authorization blocked tool execution",
366
- meta: { name, reason: relationshipDecision.reason },
367
- });
368
- return `relationship authorization required: ${relationshipDecision.reason}`;
369
- }
370
- const riskProfile = riskProfileForTool(def, name, args);
371
- const orientationPolicy = ctx?.orientationFrame?.actionPolicy;
372
- if (orientationPolicy?.mode === "correction_hold"
373
- && orientationPolicyBlocks(riskProfile, orientationPolicy.blockedMutationKinds)) {
374
- const reason = orientationPolicy.reason;
375
- const message = orientationHoldMessage(name, riskProfile, reason);
376
- (0, runtime_1.emitNervesEvent)({
377
- level: "warn",
378
- event: "tool.orientation_hold_block",
379
- component: "tools",
380
- message: "orientation hold blocked high-risk tool execution",
381
- meta: { name, mutates: mutationKindsFor(riskProfile), reason },
382
- });
383
- return message;
322
+ async function preflightToolCall(name, args, ctx) {
323
+ try {
324
+ const selection = ctx?.toolSelection;
325
+ const def = findDefinition(name, selection);
326
+ const schema = def?.tool ?? selection?.engine.find((tool) => tool.function.name === name);
327
+ if (!schema) {
328
+ (0, runtime_1.emitNervesEvent)({ level: "error", event: "tool.error", component: "tools", message: "unknown or unselected tool requested", meta: { name } });
329
+ return { kind: "rejected_before_handler", text: `unknown: ${name}` };
330
+ }
331
+ if (selection || ctx?.relationshipAuthorization) {
332
+ const validation = (0, tool_arguments_1.validateAdvertisedToolArguments)(JSON.stringify(args), schema.function.parameters ?? {});
333
+ if (!validation.ok)
334
+ return { kind: "rejected_before_handler", text: `invalid tool arguments: ${validation.reason}` };
335
+ }
336
+ (0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
337
+ const relationship = ctx?.relationshipAuthorization;
338
+ const decision = await relationship?.authorizeTool(name, args);
339
+ if (decision && (!decision.allowed || !relationship.advertisedToolNames.includes(name)
340
+ || (decision.profileId !== undefined && decision.profileId !== relationship.profileId))) {
341
+ const reason = decision.allowed ? "current relationship selection changed" : decision.reason;
342
+ (0, runtime_1.emitNervesEvent)({
343
+ level: "warn", event: "tool.relationship_authorization_block", component: "tools",
344
+ message: "relationship authorization blocked tool execution", meta: { name, reason },
345
+ });
346
+ return { kind: "rejected_before_handler", text: `relationship authorization required: ${reason}` };
347
+ }
348
+ const currentSelection = ctx?.selectCurrentTools?.()
349
+ ?? (relationship?.profileId?.startsWith("sanctuary-") || (relationship && ctx?.agentName === "sanctuary")
350
+ ? selectToolsForChannel(undefined, undefined, undefined, undefined, undefined, undefined, ctx)
351
+ : undefined);
352
+ const current = currentSelection ? findDefinition(name, currentSelection) : def?.mcpBinding ? def : findDefinition(name);
353
+ const currentSchema = current?.tool ?? currentSelection?.engine.find((tool) => tool.function.name === name)
354
+ ?? (!currentSelection ? selection?.engine.find((tool) => tool.function.name === name) : undefined);
355
+ if (!currentSchema || !(0, node_util_1.isDeepStrictEqual)(schema, currentSchema)
356
+ || (def && (!current || (def.mcpBinding
357
+ ? !current.mcpBinding || current.mcpBinding.manager !== def.mcpBinding.manager || !(0, node_util_1.isDeepStrictEqual)(current.mcpBinding, def.mcpBinding)
358
+ : current.handler !== def.handler)))) {
359
+ (0, runtime_1.emitNervesEvent)({
360
+ level: "warn", event: "tool.selection_rejected", component: "tools",
361
+ message: "selected tool is no longer current", meta: { name },
362
+ });
363
+ return { kind: "rejected_before_handler", text: `rejected: ${name} changed or is no longer available in the current tool selection` };
364
+ }
365
+ if (def?.mcpBinding) {
366
+ if (!ctx?.agentName || !ctx.agentRoot)
367
+ throw new mcp_tools_1.McpCallRejectedError("MCP requires an explicit owner");
368
+ await def.mcpBinding.manager.validateToolBinding(def.mcpBinding, { agentName: ctx.agentName, agentRoot: ctx.agentRoot });
369
+ }
370
+ if (!def)
371
+ return { kind: "ready" };
372
+ const risk = riskProfileForTool(def, name, args);
373
+ const orientationPolicy = ctx?.orientationFrame?.actionPolicy;
374
+ if (orientationPolicy?.mode === "correction_hold" && orientationPolicyBlocks(risk, orientationPolicy.blockedMutationKinds)) {
375
+ (0, runtime_1.emitNervesEvent)({
376
+ level: "warn", event: "tool.orientation_hold_block", component: "tools",
377
+ message: "orientation hold blocked high-risk tool execution",
378
+ meta: { name, mutates: mutationKindsFor(risk), reason: orientationPolicy.reason },
379
+ });
380
+ return { kind: "rejected_before_handler", text: orientationHoldMessage(name, risk, orientationPolicy.reason) };
381
+ }
382
+ const group = ctx?.context && "isGroupChat" in ctx.context ? ctx.context.isGroupChat : undefined;
383
+ const guardContext = {
384
+ readPaths: tools_base_1.editFileReadTracker, trustLevel: ctx?.context?.friend?.trustLevel,
385
+ agentRoot: ctx?.agentRoot ?? safeGetAgentRoot(), friendId: ctx?.context?.friend?.id,
386
+ ...(def?.mcpServer ? { mcpServerName: def.mcpServer } : {}),
387
+ ...(typeof group === "boolean" ? { isGroupChat: group } : {}),
388
+ };
389
+ const guard = (0, guardrails_1.guardInvocation)(name, args, guardContext);
390
+ if (!guard.allowed) {
391
+ (0, runtime_1.emitNervesEvent)({
392
+ level: "warn", event: "tool.guardrail_block", component: "tools",
393
+ message: "guardrail blocked tool execution", meta: { name, reason: guard.reason },
394
+ });
395
+ return { kind: "rejected_before_handler", text: guard.reason };
396
+ }
397
+ return { kind: "ready", definition: def, agentRoot: guardContext.agentRoot, friendId: guardContext.friendId };
384
398
  }
385
- // Guardrail check: structural + trust-level
386
- const mcpDef = mcpDefinitions.find((d) => d.tool.function.name === name);
387
- const guardContext = {
388
- readPaths: tools_base_1.editFileReadTracker,
389
- trustLevel: ctx?.context?.friend?.trustLevel,
390
- agentRoot: ctx?.agentRoot ?? safeGetAgentRoot(),
391
- friendId: ctx?.context?.friend?.id,
392
- ...(mcpDef?.mcpServer ? { mcpServerName: mcpDef.mcpServer } : {}),
393
- ...(ctx?.context?.isGroupChat !== undefined ? { isGroupChat: (ctx?.context).isGroupChat } : {}),
394
- };
395
- const guardArgs = normalizeGuardArgs(name, args);
396
- const guardResult = (0, guardrails_1.guardInvocation)(name, guardArgs, guardContext);
397
- if (!guardResult.allowed) {
399
+ catch (error) {
400
+ const text = error instanceof Error ? error.message : String(error);
398
401
  (0, runtime_1.emitNervesEvent)({
399
- level: "warn",
400
- event: "tool.guardrail_block",
401
- component: "tools",
402
- message: "guardrail blocked tool execution",
403
- meta: { name, reason: guardResult.reason },
402
+ level: "warn", event: "tool.selection_rejected", component: "tools",
403
+ message: "tool preflight failed before execution", meta: { name, reason: text.slice(0, 2000) },
404
404
  });
405
- return guardResult.reason;
405
+ return { kind: "rejected_before_handler", text: `rejected: ${text.slice(0, 2000)}`, error };
406
406
  }
407
+ }
408
+ async function executeTool(name, args, ctx, executor) {
409
+ (0, runtime_1.emitNervesEvent)({
410
+ event: "tool.start", component: "tools", message: "tool execution started",
411
+ meta: { name, ...(name === "shell" && args.command ? { command: args.command } : {}) },
412
+ });
413
+ const prepared = await preflightToolCall(name, args, ctx);
414
+ if (prepared.kind !== "ready")
415
+ return prepared;
416
+ const def = prepared.definition;
417
+ if (!def)
418
+ return { kind: "rejected_before_handler", text: `rejected: ${name} is owned by the engine` };
419
+ const guardContext = prepared;
420
+ const guardArgs = args;
407
421
  const commerceReservation = COMMERCE_AUTHORITY_TOOLS.has(name) && guardContext.agentRoot
408
422
  ? (0, store_1.reserveCommerceAuthority)({
409
423
  agentRoot: guardContext.agentRoot,
@@ -421,7 +435,7 @@ async function execTool(name, args, ctx) {
421
435
  message: "guardrail blocked tool execution",
422
436
  meta: { name, reason: commerceReservation.reason },
423
437
  });
424
- return `commerce authority required: ${commerceReservation.reason}`;
438
+ return { kind: "rejected_before_handler", text: `commerce authority required: ${commerceReservation.reason}` };
425
439
  }
426
440
  const authorizedContext = ctx;
427
441
  const toolContext = commerceReservation?.ok
@@ -435,14 +449,14 @@ async function execTool(name, args, ctx) {
435
449
  }
436
450
  : authorizedContext;
437
451
  try {
438
- const result = await def.handler(args, toolContext);
452
+ const result = await (executor ? executor(name, args, toolContext) : def.handler(args, toolContext));
439
453
  (0, runtime_1.emitNervesEvent)({
440
454
  event: "tool.end",
441
455
  component: "tools",
442
456
  message: "tool execution finished",
443
457
  meta: { name, success: true },
444
458
  });
445
- return result;
459
+ return { kind: "handler_succeeded", text: result };
446
460
  }
447
461
  catch (error) {
448
462
  (0, runtime_1.emitNervesEvent)({
@@ -452,7 +466,14 @@ async function execTool(name, args, ctx) {
452
466
  message: error instanceof Error ? error.message : String(error),
453
467
  meta: { name },
454
468
  });
455
- throw error;
469
+ const text = error instanceof Error ? error.message : String(error);
470
+ return {
471
+ kind: error instanceof mcp_tools_1.McpCallRejectedError ? "rejected_before_handler"
472
+ : error instanceof mcp_tools_1.McpToolExecutionError ? error.kind
473
+ : error instanceof tool_approval_1.ApprovalExecutionFailedError ? "handler_failed"
474
+ : riskProfileForTool(def, name, args).risk === "high" ? "handler_indeterminate" : "handler_failed",
475
+ text: text.slice(0, 2000), error,
476
+ };
456
477
  }
457
478
  finally {
458
479
  if (commerceReservation?.ok && guardContext.agentRoot) {
@@ -471,6 +492,13 @@ async function execTool(name, args, ctx) {
471
492
  }
472
493
  }
473
494
  }
495
+ async function execTool(name, args, ctx, executor) {
496
+ // Keep the legacy handler type without coercing schema-validated JSON values.
497
+ const result = await executeTool(name, args, ctx, executor);
498
+ if ("error" in result && !(result.error instanceof mcp_tools_1.McpToolExecutionError))
499
+ throw result.error;
500
+ return result.text;
501
+ }
474
502
  function summarizeKeyValues(args, keys, maxValueLength = 60) {
475
503
  const parts = [];
476
504
  for (const key of keys) {
@@ -491,8 +519,8 @@ function summarizeUnknownArgs(args) {
491
519
  return "";
492
520
  return summarizeKeyValues(args, keys);
493
521
  }
494
- function summarizeArgs(name, args) {
495
- const def = findDefinition(name);
522
+ function summarizeArgs(name, args, selection) {
523
+ const def = findDefinition(name, selection);
496
524
  if (def && def.summaryKeys !== undefined) {
497
525
  return summarizeKeyValues(args, def.summaryKeys);
498
526
  }
@@ -503,12 +531,11 @@ function summarizeArgs(name, args) {
503
531
  * For recognized tools, includes result-derived info (diff stats, exit codes, etc.).
504
532
  * Falls back to arg-based summary for unrecognized tools.
505
533
  */
506
- function buildToolResultSummary(name, args, _result, success) {
507
- /* v8 ignore start -- branches: ?? fallbacks and ternaries in tool summary formatting @preserve */
534
+ function buildToolResultSummary(name, args, _result, success, selection) {
508
535
  switch (name) {
509
536
  case "edit_file": {
510
537
  if (!success)
511
- return summarizeArgs(name, args);
538
+ return summarizeArgs(name, args, selection);
512
539
  const addedLines = (args.new_string ?? "").split("\n").length;
513
540
  const removedLines = (args.old_string ?? "").split("\n").length;
514
541
  return `+${addedLines} -${removedLines} lines in ${args.path ?? "unknown"}`;
@@ -532,7 +559,6 @@ function buildToolResultSummary(name, args, _result, success) {
532
559
  return `${taskRef} -> ${status}`;
533
560
  }
534
561
  default:
535
- return summarizeArgs(name, args);
562
+ return summarizeArgs(name, args, selection);
536
563
  }
537
- /* v8 ignore stop */
538
564
  }
@@ -1571,9 +1571,10 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1571
1571
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1572
1572
  }
1573
1573
  options.lifecycleSignal?.throwIfAborted();
1574
+ const mcpOwner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
1574
1575
  const liveTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
1575
1576
  activeTurnId = liveTurnId;
1576
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
1577
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(mcpOwner) ?? undefined;
1577
1578
  if (event.chat.isGroup) {
1578
1579
  await (0, friends_1.upsertGroupContextParticipants)({
1579
1580
  store,
@@ -1847,6 +1848,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1847
1848
  /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1848
1849
  signin: async () => undefined,
1849
1850
  ...safeToolContext,
1851
+ ...mcpOwner,
1850
1852
  summarize,
1851
1853
  bluebubblesReplyTarget: {
1852
1854
  setSelection: (selection) => replyTarget.setSelection(selection),
@@ -1880,7 +1882,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1880
1882
  orientationFrame,
1881
1883
  toolContext: {
1882
1884
  signin: async () => undefined,
1883
- agentRoot: (0, identity_1.getAgentRoot)(),
1885
+ ...mcpOwner,
1884
1886
  currentIngressEvidence: options.currentIngressEvidence,
1885
1887
  },
1886
1888
  },