@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
@@ -4,10 +4,43 @@
4
4
  * so the model can call them directly without shell indirection.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.McpToolExecutionError = exports.McpCallRejectedError = void 0;
8
+ exports.mcpToolSchema = mcpToolSchema;
7
9
  exports.mcpToolsAsDefinitions = mcpToolsAsDefinitions;
8
10
  const runtime_1 = require("../nerves/runtime");
11
+ const tool_arguments_1 = require("./tool-arguments");
12
+ class McpCallRejectedError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "McpCallRejectedError";
16
+ }
17
+ }
18
+ exports.McpCallRejectedError = McpCallRejectedError;
19
+ class McpToolExecutionError extends Error {
20
+ kind;
21
+ constructor(kind, message) {
22
+ super(message);
23
+ this.kind = kind;
24
+ this.name = "McpToolExecutionError";
25
+ }
26
+ }
27
+ exports.McpToolExecutionError = McpToolExecutionError;
28
+ function mcpToolSchema(entry, tool) {
29
+ return {
30
+ type: "function",
31
+ function: {
32
+ name: entry.pluginId
33
+ ? `mcp__${entry.server}__${tool.name}`
34
+ : tool.name.startsWith(`${entry.server}_`) || tool.name === entry.server
35
+ ? tool.name
36
+ : `${entry.server}_${tool.name}`,
37
+ description: tool.description || `MCP tool: ${tool.name} (server: ${entry.server})`,
38
+ parameters: tool.inputSchema ?? { type: "object", properties: {} },
39
+ },
40
+ };
41
+ }
9
42
  /**
10
- * Convert all tools from an McpManager into ToolDefinition objects.
43
+ * Convert an owned frozen MCP view into ToolDefinition objects.
11
44
  *
12
45
  * Naming rules:
13
46
  * - Builtin servers (agent.json `mcpServers`) — legacy `{server}_{tool}`
@@ -18,33 +51,35 @@ const runtime_1 = require("../nerves/runtime");
18
51
  * This matches Claude Code's external naming and the on-prompt promise
19
52
  * in `desk-section.ts` (`mcp__desk__*`).
20
53
  *
21
- * The handler always calls `mcpManager.callTool()` with the un-prefixed
22
- * `(server, tool)` pair regardless of how the surfaced name was shaped.
54
+ * The handler carries the exact frozen binding to the manager's client fence.
23
55
  */
24
- function mcpToolsAsDefinitions(mcpManager) {
25
- if (!mcpManager)
56
+ function mcpToolsAsDefinitions(view) {
57
+ if (!view)
26
58
  return [];
27
- return mcpManager.listAllTools().flatMap((entry) => {
28
- const isPluginSourced = Boolean(entry.pluginId);
29
- return entry.tools.map((tool) => ({
30
- tool: {
31
- type: "function",
32
- function: {
33
- name: isPluginSourced
34
- ? `mcp__${entry.server}__${tool.name}`
35
- : tool.name.startsWith(`${entry.server}_`) || tool.name === entry.server
36
- ? tool.name
37
- : `${entry.server}_${tool.name}`,
38
- description: tool.description || `MCP tool: ${tool.name} (server: ${entry.server})`,
39
- parameters: tool.inputSchema ?? { type: "object", properties: {} },
40
- },
41
- },
59
+ return view.entries.flatMap((entry) => entry.tools.map((tool) => {
60
+ const schema = mcpToolSchema(entry, tool);
61
+ const binding = Object.freeze({
62
+ manager: view.manager, ...view.owner, server: entry.server,
63
+ rawName: tool.name, surfacedName: schema.function.name,
64
+ source: entry.source, pluginId: entry.pluginId,
65
+ configDigest: entry.configDigest, generation: entry.generation,
66
+ schemaDigest: (0, tool_arguments_1.digestJson)(schema),
67
+ });
68
+ return {
69
+ tool: schema,
42
70
  riskProfile: {
43
71
  mutates: "external_side_effect",
44
72
  risk: "high",
45
73
  reason: "MCP tools may mutate external systems",
46
74
  },
47
- handler: async (args) => {
75
+ handler: async (args, ctx) => {
76
+ if (ctx?.agentName !== binding.agentName || ctx.agentRoot !== binding.agentRoot) {
77
+ (0, runtime_1.emitNervesEvent)({
78
+ level: "warn", event: "mcp.tool_rejected", component: "repertoire",
79
+ message: "MCP tool owner does not match its caller", meta: { reason: "owner mismatch" },
80
+ });
81
+ throw new McpCallRejectedError("MCP tool owner does not match its caller");
82
+ }
48
83
  (0, runtime_1.emitNervesEvent)({
49
84
  event: "mcp.tool_start",
50
85
  component: "repertoire",
@@ -52,11 +87,14 @@ function mcpToolsAsDefinitions(mcpManager) {
52
87
  meta: { server: entry.server, tool: tool.name },
53
88
  });
54
89
  try {
55
- const result = await mcpManager.callTool(entry.server, tool.name, args);
90
+ const result = await view.manager.callTool(binding, args, { agentName: ctx.agentName, agentRoot: ctx.agentRoot });
56
91
  const text = result.content
57
92
  .filter((c) => c.type === "text" && c.text)
58
93
  .map((c) => c.text)
59
94
  .join("");
95
+ if (result.isError === true) {
96
+ throw new McpToolExecutionError("handler_failed", `[mcp error] ${entry.server}/${tool.name}: ${text}`);
97
+ }
60
98
  (0, runtime_1.emitNervesEvent)({
61
99
  event: "mcp.tool_end",
62
100
  component: "repertoire",
@@ -74,10 +112,13 @@ function mcpToolsAsDefinitions(mcpManager) {
74
112
  message: `MCP tool ${entry.server}/${tool.name} failed: ${reason}`,
75
113
  meta: { server: entry.server, tool: tool.name, reason },
76
114
  });
77
- return `[mcp error] ${entry.server}/${tool.name}: ${reason}`;
115
+ if (error instanceof McpCallRejectedError || error instanceof McpToolExecutionError)
116
+ throw error;
117
+ throw new McpToolExecutionError("handler_indeterminate", `[mcp error] ${entry.server}/${tool.name}: ${reason}`);
78
118
  }
79
119
  },
80
120
  mcpServer: entry.server,
81
- }));
82
- });
121
+ mcpBinding: binding,
122
+ };
123
+ }));
83
124
  }
@@ -104,20 +104,20 @@ function readPluginMcpManifest(pluginRoot) {
104
104
  *
105
105
  * `homeDir` is an optional override for the `~/.ouro-cli/` root (test-only).
106
106
  */
107
- function listPluginMcpServers(homeDir) {
107
+ function listPluginMcpServers(homeDir, owner) {
108
108
  (0, runtime_1.emitNervesEvent)({
109
109
  event: "plugin_mcp.list_start",
110
110
  component: "repertoire",
111
111
  message: "discovering plugin-declared MCP servers",
112
112
  meta: { operation: "listPluginMcpServers" },
113
113
  });
114
- const config = (0, identity_1.loadAgentConfig)();
114
+ const config = owner === undefined ? (0, identity_1.loadAgentConfig)() : (0, identity_1.loadAgentConfig)(owner);
115
115
  const declaredPlugins = config.plugins ?? [];
116
116
  const pluginsRoot = (0, plugins_1.getPluginsRoot)(homeDir);
117
117
  // Per-agent override: DESK defaults to <bundleRoot>/desk/ when not explicitly set.
118
118
  const overrides = {};
119
119
  if (process.env.DESK === undefined) {
120
- overrides.DESK = path.join((0, identity_1.getAgentRoot)(), "desk");
120
+ overrides.DESK = path.join(owner === undefined ? (0, identity_1.getAgentRoot)() : owner.agentRoot, "desk");
121
121
  }
122
122
  const out = [];
123
123
  for (const plugin of declaredPlugins) {
@@ -11,12 +11,13 @@ const crypto_1 = require("crypto");
11
11
  const runtime_1 = require("../nerves/runtime");
12
12
  const sessions = new Map();
13
13
  const MAX_OUTPUT_LINES = 200;
14
- function spawnBackgroundShell(command) {
14
+ function spawnBackgroundShell(command, owner) {
15
15
  const id = (0, crypto_1.randomUUID)();
16
16
  const proc = (0, child_process_1.spawn)("sh", ["-c", command], {
17
17
  stdio: ["pipe", "pipe", "pipe"],
18
18
  });
19
19
  const session = {
20
+ owner: Object.freeze({ ...owner }),
20
21
  process: proc,
21
22
  info: {
22
23
  id,
@@ -61,9 +62,9 @@ function spawnBackgroundShell(command) {
61
62
  });
62
63
  return { ...session.info };
63
64
  }
64
- function getShellSession(id) {
65
+ function getShellSession(id, owner) {
65
66
  const session = sessions.get(id);
66
- if (!session)
67
+ if (!session || session.owner.agentName !== owner.agentName || session.owner.agentRoot !== owner.agentRoot)
67
68
  return undefined;
68
69
  (0, runtime_1.emitNervesEvent)({
69
70
  component: "repertoire",
@@ -73,8 +74,8 @@ function getShellSession(id) {
73
74
  });
74
75
  return { ...session.info };
75
76
  }
76
- function listShellSessions() {
77
- return Array.from(sessions.values()).map((s) => ({
77
+ function listShellSessions(owner) {
78
+ return Array.from(sessions.values()).filter((session) => session.owner.agentName === owner.agentName && session.owner.agentRoot === owner.agentRoot).map((s) => ({
78
79
  id: s.info.id,
79
80
  command: s.info.command,
80
81
  status: s.info.status,
@@ -84,9 +85,9 @@ function listShellSessions() {
84
85
  output: [], // Don't include full output in listing
85
86
  }));
86
87
  }
87
- function tailShellSession(id, lines = 50) {
88
+ function tailShellSession(id, owner, lines = 50) {
88
89
  const session = sessions.get(id);
89
- if (!session)
90
+ if (!session || session.owner.agentName !== owner.agentName || session.owner.agentRoot !== owner.agentRoot)
90
91
  return undefined;
91
92
  const tail = session.info.output.slice(-lines);
92
93
  (0, runtime_1.emitNervesEvent)({
@@ -3,18 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.assertRelationshipToolOwner = assertRelationshipToolOwner;
6
7
  exports.digestJson = digestJson;
8
+ exports.freezeToolValue = freezeToolValue;
7
9
  exports.validateAdvertisedToolArguments = validateAdvertisedToolArguments;
8
10
  const node_crypto_1 = require("node:crypto");
11
+ const node_path_1 = require("node:path");
9
12
  const ajv_1 = __importDefault(require("ajv"));
10
13
  const runtime_1 = require("../nerves/runtime");
11
- const ajv = new ajv_1.default({
12
- strict: true,
13
- allErrors: true,
14
- coerceTypes: false,
15
- removeAdditional: false,
16
- useDefaults: false,
17
- });
14
+ function assertRelationshipToolOwner(ctx) {
15
+ if (ctx?.relationshipAuthorization && (typeof ctx.agentName !== "string" || ctx.agentName.length === 0
16
+ || typeof ctx.agentRoot !== "string" || !(0, node_path_1.isAbsolute)(ctx.agentRoot))) {
17
+ (0, runtime_1.emitNervesEvent)({
18
+ level: "warn", component: "tools", event: "tool.owner_context_rejected",
19
+ message: "relationship-scoped tool has no valid explicit owner",
20
+ meta: { reason: "owner_coordinates_unavailable" },
21
+ });
22
+ throw new Error("an explicit owner name and absolute root are required for relationship-scoped tools");
23
+ }
24
+ }
18
25
  const validators = new WeakMap();
19
26
  function unsupportedSchemaFeature(value, seen = new WeakSet()) {
20
27
  if (value === null)
@@ -65,6 +72,14 @@ function renderErrors(errors) {
65
72
  function digestJson(value) {
66
73
  return digest(canonicalize(value));
67
74
  }
75
+ function freezeToolValue(value) {
76
+ if (value !== null && typeof value === "object") {
77
+ for (const child of Object.values(value))
78
+ freezeToolValue(child);
79
+ Object.freeze(value);
80
+ }
81
+ return value;
82
+ }
68
83
  function validateAdvertisedToolArguments(rawArguments, schema) {
69
84
  const unsupported = unsupportedSchemaFeature(schema);
70
85
  if (unsupported) {
@@ -103,7 +118,14 @@ function validateAdvertisedToolArguments(rawArguments, schema) {
103
118
  }
104
119
  let validator = validators.get(schema);
105
120
  try {
106
- validator ??= ajv.compile(schema);
121
+ // Each snapshot owns its compiler registry; only the weak cache is shared.
122
+ validator ??= new ajv_1.default({
123
+ strict: true,
124
+ allErrors: true,
125
+ coerceTypes: false,
126
+ removeAdditional: false,
127
+ useDefaults: false,
128
+ }).compile(schema);
107
129
  validators.set(schema, validator);
108
130
  }
109
131
  catch (error) {
@@ -56,6 +56,7 @@ const obligations_1 = require("../arc/obligations");
56
56
  const progress_story_1 = require("../heart/progress-story");
57
57
  const habit_session_summary_1 = require("../heart/habits/habit-session-summary");
58
58
  const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
59
+ const tool_arguments_1 = require("./tool-arguments");
59
60
  const mail_import_discovery_1 = require("../heart/mail-import-discovery");
60
61
  const outbound_1 = require("../senses/voice/outbound");
61
62
  const router_1 = require("../heart/external-events/router");
@@ -671,7 +672,7 @@ exports.sessionToolDefinitions = [
671
672
  },
672
673
  },
673
674
  },
674
- handler: (args) => {
675
+ handler: (args, ctx) => {
675
676
  const validation = validateSessionSummarySelector(args);
676
677
  if (!validation.ok) {
677
678
  return JSON.stringify({
@@ -680,7 +681,8 @@ exports.sessionToolDefinitions = [
680
681
  message: validation.message,
681
682
  }, null, 2);
682
683
  }
683
- const summary = (0, habit_session_summary_1.readHabitSessionSummary)((0, identity_1.getAgentRoot)(), validation.selector);
684
+ (0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
685
+ const summary = (0, habit_session_summary_1.readHabitSessionSummary)(ctx?.agentRoot ?? (0, identity_1.getAgentRoot)(ctx?.agentName), validation.selector);
684
686
  if (!summary) {
685
687
  return JSON.stringify({
686
688
  kind: "not_found",
@@ -731,6 +733,14 @@ exports.sessionToolDefinitions = [
731
733
  const key = args.key || "session";
732
734
  const count = parseInt(args.messageCount || "20", 10);
733
735
  const mode = args.mode || "transcript";
736
+ if (!(0, config_1.isValidSessionCoordinate)(friendId) || !(0, config_1.isValidSessionCoordinate)(channel)) {
737
+ (0, runtime_1.emitNervesEvent)({
738
+ level: "warn", component: "tools", event: "tool.session_path_rejected",
739
+ message: "session query rejected invalid coordinates", meta: { reason: "invalid friend or channel" },
740
+ });
741
+ return NO_SESSION_FOUND_MESSAGE;
742
+ }
743
+ (0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
734
744
  // Resolve friend name -> UUID if not already a UUID or "self"
735
745
  if (friendId && friendId !== "self" && !/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(friendId) && ctx?.friendStore?.listAll) {
736
746
  const allFriends = await ctx.friendStore.listAll();
@@ -743,8 +753,9 @@ exports.sessionToolDefinitions = [
743
753
  if (friendId !== "self" || channel !== "inner") {
744
754
  return "status mode is only available for self/private runtime.";
745
755
  }
746
- const sessionPath = (0, thoughts_1.getPrivateRuntimeSessionPath)((0, identity_1.getAgentRoot)());
747
- const pendingDir = (0, pending_1.getPrivateRuntimePendingDir)((0, identity_1.getAgentName)());
756
+ const agentRoot = ctx?.agentRoot ?? (0, identity_1.getAgentRoot)(ctx?.agentName);
757
+ const sessionPath = (0, thoughts_1.getPrivateRuntimeSessionPath)(agentRoot);
758
+ const pendingDir = (0, pending_1.getPrivateRuntimePendingDir)(ctx?.agentName ?? (0, identity_1.getAgentName)(), agentRoot);
748
759
  return renderInnerProgressStatus((0, thoughts_1.readPrivateRuntimeStatus)(sessionPath, pendingDir));
749
760
  }
750
761
  if (mode === "search") {
@@ -754,7 +765,21 @@ exports.sessionToolDefinitions = [
754
765
  removalCycle: "alpha.616",
755
766
  });
756
767
  }
757
- const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
768
+ let sessFile;
769
+ try {
770
+ sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key, {
771
+ agentRoot: ctx?.agentRoot ?? (0, identity_1.getAgentRoot)(ctx?.agentName), confined: true,
772
+ });
773
+ }
774
+ catch (error) {
775
+ if (!(error instanceof config_1.InvalidSessionPathError))
776
+ throw error;
777
+ (0, runtime_1.emitNervesEvent)({
778
+ level: "warn", component: "tools", event: "tool.session_path_rejected",
779
+ message: "session query rejected an unconfined path", meta: { reason: error.message },
780
+ });
781
+ return NO_SESSION_FOUND_MESSAGE;
782
+ }
758
783
  const sessionTail = await summarizeSessionTailSafely({
759
784
  sessionPath: sessFile,
760
785
  friendId,
@@ -5,13 +5,19 @@ const child_process_1 = require("child_process");
5
5
  const shell_sessions_1 = require("./shell-sessions");
6
6
  const identity_1 = require("../heart/identity");
7
7
  const runtime_1 = require("../nerves/runtime");
8
+ const tool_arguments_1 = require("./tool-arguments");
9
+ function shellOwner(ctx) {
10
+ (0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
11
+ const agentName = ctx?.agentName ?? (0, identity_1.getAgentName)();
12
+ return { agentName, agentRoot: ctx?.agentRoot ?? (0, identity_1.getAgentRoot)(agentName) };
13
+ }
8
14
  exports.shellToolDefinitions = [
9
15
  {
10
16
  tool: {
11
17
  type: "function",
12
18
  function: {
13
19
  name: "shell",
14
- description: "Run a shell command and return stdout/stderr. Working directory persists between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
20
+ description: "Run a shell command and return stdout/stderr. Each call starts in the runtime's working directory; cd does not persist between calls. Use dedicated tools instead of shell when available: read_file instead of cat, edit_file instead of sed, glob instead of find, grep instead of grep/rg. Reserve shell for operations that genuinely need the shell: installing packages, running builds/tests, git operations, process management. Be careful with destructive commands -- consider reversibility before running. If a command fails, read the error output before retrying with a different approach.",
15
21
  parameters: {
16
22
  type: "object",
17
23
  properties: {
@@ -22,7 +28,7 @@ exports.shellToolDefinitions = [
22
28
  },
23
29
  background: {
24
30
  type: "boolean",
25
- description: "Run in background. Returns immediately with a process ID. Use shell_status/shell_tail to monitor.",
31
+ description: "Run in background and return immediately with a process ID. Records belong to the initiating agent and live only in process memory. Use separately authorized shell_status/shell_tail calls to monitor. Revocation does not kill a started process; runtime restart loses its record.",
26
32
  },
27
33
  },
28
34
  required: ["command"],
@@ -42,7 +48,8 @@ exports.shellToolDefinitions = [
42
48
  }
43
49
  return { kind: "not_required" };
44
50
  },
45
- handler: (a) => {
51
+ handler: (a, ctx) => {
52
+ (0, tool_arguments_1.assertRelationshipToolOwner)(ctx);
46
53
  // Destructive pattern detection (friction, not a block)
47
54
  const destructivePatterns = (0, shell_sessions_1.detectDestructivePatterns)(a.command);
48
55
  if (destructivePatterns.length > 0) {
@@ -55,17 +62,26 @@ exports.shellToolDefinitions = [
55
62
  });
56
63
  }
57
64
  // Background mode: spawn and return immediately
58
- if (a.background === "true") {
59
- const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command);
65
+ if (a.background === true) {
66
+ const session = (0, shell_sessions_1.spawnBackgroundShell)(a.command, shellOwner(ctx));
60
67
  return JSON.stringify({ id: session.id, command: session.command, status: session.status });
61
68
  }
62
69
  const MAX_TIMEOUT = 600000;
63
70
  const requestedTimeout = Number(a.timeout_ms) || 0;
64
71
  let configDefault = 30000;
72
+ const owner = ctx?.agentName !== undefined || ctx?.agentRoot !== undefined ? shellOwner(ctx) : undefined;
65
73
  try {
66
- configDefault = (0, identity_1.loadAgentConfig)().shell?.defaultTimeout ?? 30000;
74
+ configDefault = (0, identity_1.loadAgentConfig)(owner).shell?.defaultTimeout ?? 30000;
75
+ }
76
+ catch (error) {
77
+ if (owner)
78
+ throw error;
79
+ (0, runtime_1.emitNervesEvent)({
80
+ level: "warn", component: "tools", event: "tool.shell_config_unavailable",
81
+ message: "legacy shell caller has no configuration; using the default timeout",
82
+ meta: { fallbackTimeout: configDefault },
83
+ });
67
84
  }
68
- catch { /* test env: no --agent flag */ }
69
85
  const baseTimeout = requestedTimeout > 0 ? requestedTimeout : configDefault;
70
86
  const timeout = Math.min(baseTimeout, MAX_TIMEOUT);
71
87
  const output = (0, child_process_1.execSync)(a.command, {
@@ -84,7 +100,7 @@ exports.shellToolDefinitions = [
84
100
  type: "function",
85
101
  function: {
86
102
  name: "shell_status",
87
- description: "Check status of background shell processes. Omit id to list all.",
103
+ description: "Check separately authorized status of background shell processes owned by this agent. Omit id to list this agent's records. Records live only in process memory; runtime restart loses observability, and revoked access does not kill a started process.",
88
104
  parameters: {
89
105
  type: "object",
90
106
  properties: {
@@ -93,11 +109,11 @@ exports.shellToolDefinitions = [
93
109
  },
94
110
  },
95
111
  },
96
- handler: (a) => {
112
+ handler: (a, ctx) => {
97
113
  if (!a.id) {
98
- return JSON.stringify((0, shell_sessions_1.listShellSessions)());
114
+ return JSON.stringify((0, shell_sessions_1.listShellSessions)(shellOwner(ctx)));
99
115
  }
100
- const session = (0, shell_sessions_1.getShellSession)(a.id);
116
+ const session = (0, shell_sessions_1.getShellSession)(a.id, shellOwner(ctx));
101
117
  if (!session)
102
118
  return `process not found: ${a.id}`;
103
119
  return JSON.stringify(session);
@@ -109,7 +125,7 @@ exports.shellToolDefinitions = [
109
125
  type: "function",
110
126
  function: {
111
127
  name: "shell_tail",
112
- description: "Show recent output from a background shell process.",
128
+ description: "Show recent output from a background shell process owned by this agent. Access is reauthorized on each call. Revocation does not kill a started process, and runtime restart loses the in-memory record.",
113
129
  parameters: {
114
130
  type: "object",
115
131
  properties: {
@@ -119,11 +135,11 @@ exports.shellToolDefinitions = [
119
135
  },
120
136
  },
121
137
  },
122
- handler: (a) => {
138
+ handler: (a, ctx) => {
123
139
  /* v8 ignore next -- schema requires id, defensive guard @preserve */
124
140
  if (!a.id)
125
141
  return "id is required";
126
- const output = (0, shell_sessions_1.tailShellSession)(a.id);
142
+ const output = (0, shell_sessions_1.tailShellSession)(a.id, shellOwner(ctx));
127
143
  if (output === undefined)
128
144
  return `process not found: ${a.id}`;
129
145
  return output || "(no output yet)";
@@ -98,12 +98,12 @@ exports.voiceToolDefinitions = [{
98
98
  const source = args.source === "url" || args.source === "file" || args.source === "tone"
99
99
  ? args.source
100
100
  : "tone";
101
- const durationMs = typeof args.durationMs === "string" && args.durationMs.trim()
102
- ? Number(args.durationMs)
103
- : undefined;
104
- const toneHz = typeof args.toneHz === "string" && args.toneHz.trim()
105
- ? Number(args.toneHz)
106
- : undefined;
101
+ const duration = args.durationMs;
102
+ const frequency = args.toneHz;
103
+ const durationMs = typeof duration === "number" ? duration
104
+ : typeof duration === "string" && duration.trim() ? Number(duration) : undefined;
105
+ const toneHz = typeof frequency === "number" ? frequency
106
+ : typeof frequency === "string" && frequency.trim() ? Number(frequency) : undefined;
107
107
  const request = {
108
108
  source,
109
109
  /* v8 ignore next -- sparse playback argument permutations are covered in the transport-level voice_play_audio tests @preserve */