@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.
- package/changelog.json +18 -0
- package/deploy/unraid/README.txt +3 -6
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
- package/deploy/unraid/sanctuary-deployment-target.mjs +1 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/config.js +32 -2
- package/dist/heart/core.js +167 -115
- package/dist/heart/daemon/daemon.js +19 -9
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
- package/dist/heart/frontend-approval-runtime.js +87 -13
- package/dist/heart/identity.js +17 -5
- package/dist/heart/session-events.js +8 -1
- package/dist/heart/tool-approval.js +64 -31
- package/dist/mind/pending.js +4 -4
- package/dist/repertoire/mcp-manager.js +365 -381
- package/dist/repertoire/mcp-tools.js +66 -25
- package/dist/repertoire/plugin-mcp.js +3 -3
- package/dist/repertoire/shell-sessions.js +8 -7
- package/dist/repertoire/tool-arguments.js +30 -8
- package/dist/repertoire/tools-session.js +30 -5
- package/dist/repertoire/tools-shell.js +30 -14
- package/dist/repertoire/tools-voice.js +6 -6
- package/dist/repertoire/tools.js +231 -205
- package/dist/senses/bluebubbles/index.js +4 -2
- package/dist/senses/cli.js +6 -5
- package/dist/senses/private-runtime.js +9 -3
- package/dist/senses/shared-turn.js +12 -9
- package/dist/senses/teams.js +15 -12
- package/dist/senses/telegram-approval-runtime.js +129 -20
- package/dist/senses/telegram-client.js +19 -2
- package/dist/senses/telegram.js +54 -15
- package/dist/senses/voice/twilio-phone.js +123 -173
- package/npm-shrinkwrap.json +2 -2
- 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
|
|
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
|
|
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(
|
|
25
|
-
if (!
|
|
56
|
+
function mcpToolsAsDefinitions(view) {
|
|
57
|
+
if (!view)
|
|
26
58
|
return [];
|
|
27
|
-
return
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
747
|
-
const
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 ===
|
|
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
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 */
|