@ducci/jarvis 1.0.24 → 1.0.26
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/docs/agent.md +17 -0
- package/docs/findings/008-exec-timeout-architecture.md +118 -0
- package/docs/findings/009-non-string-response-field.md +153 -0
- package/docs/system-prompt.md +18 -0
- package/package.json +1 -1
- package/src/channels/telegram/index.js +5 -2
- package/src/server/agent.js +6 -2
- package/src/server/tools.js +7 -2
package/docs/agent.md
CHANGED
|
@@ -503,6 +503,23 @@ Schema:
|
|
|
503
503
|
- No additional per-run tool-call cap beyond iterations.
|
|
504
504
|
- No token limit enforcement in v1.
|
|
505
505
|
|
|
506
|
+
### Two-Level Tool Timeout Architecture
|
|
507
|
+
|
|
508
|
+
Every tool execution is governed by two independent timeout layers:
|
|
509
|
+
|
|
510
|
+
**Layer 1 — Outer wrapper** (`executeTool` in `src/server/tools.js`):
|
|
511
|
+
```js
|
|
512
|
+
const timeoutMs = tool.timeout || TOOL_TIMEOUT_MS; // TOOL_TIMEOUT_MS = 60_000
|
|
513
|
+
return await Promise.race([fn(toolArgs, ...), timeout]);
|
|
514
|
+
```
|
|
515
|
+
This is the hard cap. `tool.timeout` is a top-level property on the tool registry entry (not inside `definition` or `code`). `exec` has `timeout: 300_000` (5 minutes); `system_install` also has 5 minutes; all other tools default to 60s.
|
|
516
|
+
|
|
517
|
+
**Layer 2 — Inner timeout** (inside the tool's `code`): e.g. `execAsync(cmd, { timeout: 270000 })`. Should be slightly shorter than Layer 1 to ensure a clean error from the inner call rather than a hard kill from the outer wrapper.
|
|
518
|
+
|
|
519
|
+
**Declaring a custom timeout on a tool** (via `save_tool`): pass the optional `timeout` parameter (in ms, capped at 600,000). This writes the top-level `timeout` property to the tool entry in `tools.json`.
|
|
520
|
+
|
|
521
|
+
**Important**: Modifying a seed tool's code via `save_tool` does NOT change its outer timeout — seed tools are restored to their original definition on server restart via `seedTools()`.
|
|
522
|
+
|
|
506
523
|
## User Info
|
|
507
524
|
|
|
508
525
|
User info is stored as a small JSON file in the Jarvis data directory: `~/.jarvis/data/user-info.json`. `save_user_info` appends to the collection, and `read_user_info` returns the full set.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Finding 008: exec Timeout Architecture — Agent Cannot Increase Its Own Timeout
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-02-28
|
|
4
|
+
**Severity:** Medium — caused 5 wasted user interactions and agent confusion; no crashes or data loss
|
|
5
|
+
**Status:** Fixed
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What Happened
|
|
10
|
+
|
|
11
|
+
A user asked Jarvis to run a cybersecurity scan script (`nuclei` + `nmap`) against `https://dviet.de`. The script ran via the `exec` tool and timed out after 60 seconds. The user then asked the agent to "increase the timeout to 5 minutes."
|
|
12
|
+
|
|
13
|
+
The agent attempted this in two ways:
|
|
14
|
+
|
|
15
|
+
1. **Run 11**: Used `exec` to run `sed -i 's/"timeout": 60000/"timeout": 300000/g' tools.json` — changing the `timeout` value inside the exec tool's `code` string from 60s to 300s.
|
|
16
|
+
2. **Run 13**: Called `save_tool` to recreate the exec tool with a "5-minute timeout" description and modified code.
|
|
17
|
+
|
|
18
|
+
Both attempts failed. The scan timed out at 60s in every subsequent run. The agent and user concluded "the platform enforces a 60-second cap" — true, but neither understood why the agent's changes had no effect.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Root Cause
|
|
23
|
+
|
|
24
|
+
### The Two-Level Timeout Architecture
|
|
25
|
+
|
|
26
|
+
Every tool execution is governed by two independent timeouts:
|
|
27
|
+
|
|
28
|
+
**Layer 1 — Outer wrapper** (`executeTool` in `src/server/tools.js`):
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const timeoutMs = tool.timeout || TOOL_TIMEOUT_MS; // TOOL_TIMEOUT_MS = 60_000
|
|
32
|
+
const timeout = new Promise((_, reject) =>
|
|
33
|
+
setTimeout(() => reject(new Error(`Tool '${name}' timed out after ${timeoutMs / 1000}s`)), timeoutMs)
|
|
34
|
+
);
|
|
35
|
+
return await Promise.race([fn(toolArgs, ...), timeout]);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`tool.timeout` is a **top-level property on the tool registry entry** — not inside `definition` or `code`. Only `system_install` had this property set (to 300,000ms). The exec tool had no top-level `timeout`, so it always used the 60s default.
|
|
39
|
+
|
|
40
|
+
**Layer 2 — Inner timeout** (inside the tool's `code`):
|
|
41
|
+
|
|
42
|
+
The exec seed tool's code contains `execAsync(args.cmd, { timeout: 60000 })`. This is just a string stored in tools.json. Changing this number (via sed or save_tool) only affects the inner `execAsync` behavior — the outer Promise.race at 60s fires first anyway.
|
|
43
|
+
|
|
44
|
+
### Why the Agent's Fixes Had No Effect
|
|
45
|
+
|
|
46
|
+
1. **sed on the code string**: Changed the inner `execAsync` timeout from 60s to 300s. But the outer wrapper uses `tool.timeout || 60_000`, and `exec` has no `tool.timeout` property. The outer race still fires at 60s and wins.
|
|
47
|
+
|
|
48
|
+
2. **`save_tool` recreation**: `save_tool` writes `{ definition, code }` to tools.json — it had no `timeout` parameter. The exec entry still had no top-level `timeout` after `save_tool`. Same result.
|
|
49
|
+
|
|
50
|
+
### Why seedTools() Makes This Permanent
|
|
51
|
+
|
|
52
|
+
Even if a manual edit to tools.json successfully added `exec.timeout = 300000`, the next server restart would run `seedTools()`, compare against `SEED_TOOLS.exec` (which has no timeout), and restore it — losing the change.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What Changed
|
|
57
|
+
|
|
58
|
+
### 1. `exec` seed tool now has a 5-minute timeout (`src/server/tools.js`)
|
|
59
|
+
|
|
60
|
+
Added `timeout: 300_000` as a top-level property on the exec seed tool — the simplest fix:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
exec: {
|
|
64
|
+
timeout: 300_000, // 5 minutes — Layer 1 outer wrapper reads this
|
|
65
|
+
definition: { ... },
|
|
66
|
+
code: `... execAsync(args.cmd, { timeout: 270000 }) ...` // 4.5 min inner, leaves headroom
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The inner `execAsync` timeout was also updated from 60s to 270s (4.5 min) so it fires cleanly before the outer wrapper, giving a proper error message rather than a hard kill.
|
|
71
|
+
|
|
72
|
+
### 2. `save_tool` now accepts a `timeout` parameter (`src/server/tools.js`)
|
|
73
|
+
|
|
74
|
+
The `save_tool` tool now accepts an optional `timeout` field (in milliseconds, max 600,000 = 10 minutes). When provided, it is written as a top-level property on the tool entry:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const entry = { definition: { ... }, code: args.code };
|
|
78
|
+
if (args.timeout !== undefined) {
|
|
79
|
+
const t = Number(args.timeout);
|
|
80
|
+
entry.timeout = Math.min(t, 600_000);
|
|
81
|
+
}
|
|
82
|
+
tools[args.name] = entry;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This allows the agent to create custom tools with explicit timeout declarations when wrapping slow operations.
|
|
86
|
+
|
|
87
|
+
### 3. System prompt updated (`docs/system-prompt.md`)
|
|
88
|
+
|
|
89
|
+
Added a `## Execution Timeouts` section documenting:
|
|
90
|
+
- `exec` = 5-minute cap (covers scans, builds, and most long-running commands)
|
|
91
|
+
- `system_install` = 5-minute cap, use for package installation
|
|
92
|
+
- Custom tools via `save_tool` = 60s default, pass `timeout` param to extend
|
|
93
|
+
- Background execution pattern for processes > 5 minutes
|
|
94
|
+
|
|
95
|
+
### 4. `agent.md` updated (`docs/agent.md`)
|
|
96
|
+
|
|
97
|
+
Added a `### Two-Level Tool Timeout Architecture` subsection under `## Limits and Timeouts` explaining:
|
|
98
|
+
- The outer wrapper and how `tool.timeout` is read
|
|
99
|
+
- The inner timeout in tool code and its relationship to the outer
|
|
100
|
+
- How to declare a custom timeout via `save_tool`
|
|
101
|
+
- Why seed tool modifications via `save_tool` don't change the outer timeout (seedTools() restores on restart)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## What Was Not Changed
|
|
106
|
+
|
|
107
|
+
- `TOOL_TIMEOUT_MS` constant — remains at 60,000ms (the default for tools without an explicit timeout)
|
|
108
|
+
- `system_install` — unchanged
|
|
109
|
+
- The handoff system, checkpoint memory, loop detection — all unchanged
|
|
110
|
+
- `seedTools()` update detection logic — unchanged
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Outcome
|
|
115
|
+
|
|
116
|
+
- `exec` now has a 5-minute timeout — long-running scans, builds, and downloads work without any workaround
|
|
117
|
+
- The agent can set custom timeouts on tools it creates via `save_tool`
|
|
118
|
+
- The system prompt and docs explain the architecture so the agent doesn't waste iterations on an unsolvable problem
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Finding 009: Non-String `response` Field Crashes Telegram Delivery
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-03-01
|
|
4
|
+
**Severity:** High — caused "Sorry, something went wrong sending the response" with no useful information for the user
|
|
5
|
+
**Status:** Fixed
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Observed Session
|
|
10
|
+
|
|
11
|
+
The session ran 19 agent runs, all completing successfully (`ok` or `checkpoint_reached`). The crash occurred on run 19. The user asked:
|
|
12
|
+
|
|
13
|
+
> "List me all tool calls you did In this session. Tool name and args are enough to display for each entry."
|
|
14
|
+
|
|
15
|
+
The model returned valid JSON but placed the list of tool calls as a JSON **array** (not a string) in the `response` field:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"response": [
|
|
20
|
+
{ "tool": "exec", "args": { "cmd": "find ..." } },
|
|
21
|
+
...16 entries...
|
|
22
|
+
],
|
|
23
|
+
"logSummary": "Enumerated every tool call made during the session..."
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The Telegram user received: **"Sorry, something went wrong sending the response. Please try again."**
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Bug Chain
|
|
32
|
+
|
|
33
|
+
### Step 1 — Agent parses valid JSON, stores non-string response
|
|
34
|
+
|
|
35
|
+
`runAgentLoop` in `src/server/agent.js` successfully parsed the model's response JSON. The extraction logic had no type check:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
response = parsed.response || content;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`parsed.response` was an array (truthy) → `response` was set to the array. No validation. The array propagated through `finalResponse` all the way to the return value of `handleChat`.
|
|
42
|
+
|
|
43
|
+
### Step 2 — Telegram handler crashes calling `.trim()` on an array
|
|
44
|
+
|
|
45
|
+
In `src/channels/telegram/index.js`:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const text = result.response?.trim()
|
|
49
|
+
|| 'The agent encountered an error...';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`?.` guards against `null` and `undefined` only — not against wrong types. Arrays do not have a `.trim()` method. This threw:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
TypeError: result.response.trim is not a function
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step 3 — Delivery catch block sends the generic error
|
|
59
|
+
|
|
60
|
+
The TypeError was caught by the outer delivery try/catch, which replied:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Sorry, something went wrong sending the response. Please try again.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The user had no idea what failed. The agent had completed successfully — only the delivery step crashed.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Root Causes
|
|
71
|
+
|
|
72
|
+
**Primary**: `agent.js` never validates that `parsed.response` is a string after JSON parsing. The response contract ("Your message to the user, in plain text.") is documented but never enforced. Any JSON value — array, object, number, null — passes through silently.
|
|
73
|
+
|
|
74
|
+
**Secondary**: `telegram/index.js` assumed `result.response` would always be a string or null/undefined, and called `.trim()` without type-guarding.
|
|
75
|
+
|
|
76
|
+
The same primary bug exists in the wrap-up path (line ~315):
|
|
77
|
+
```js
|
|
78
|
+
response = parsedWrapUp.response || '';
|
|
79
|
+
```
|
|
80
|
+
This would fail identically if the wrap-up model returned a non-string `response`.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## What Was Not Caught Earlier
|
|
85
|
+
|
|
86
|
+
- The JSONL log stored `response: [array]` but the run status was `ok` — nothing flagged as an error on the agent side.
|
|
87
|
+
- The error only surfaces in the Telegram delivery layer, which has no visibility into the JSONL log.
|
|
88
|
+
- The model had valid intent (listing tool calls as a structured data type) — it just put the data in the wrong JSON field type.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Fix
|
|
93
|
+
|
|
94
|
+
### 1. `src/server/agent.js` — normalize response to string at both sites
|
|
95
|
+
|
|
96
|
+
**Main response path:**
|
|
97
|
+
```js
|
|
98
|
+
// Before:
|
|
99
|
+
response = parsed.response || content;
|
|
100
|
+
|
|
101
|
+
// After:
|
|
102
|
+
response = typeof parsed.response === 'string'
|
|
103
|
+
? parsed.response
|
|
104
|
+
: JSON.stringify(parsed.response, null, 2);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Wrap-up path:**
|
|
108
|
+
```js
|
|
109
|
+
// Before:
|
|
110
|
+
response = parsedWrapUp.response || '';
|
|
111
|
+
|
|
112
|
+
// After:
|
|
113
|
+
response = typeof parsedWrapUp.response === 'string'
|
|
114
|
+
? parsedWrapUp.response
|
|
115
|
+
: parsedWrapUp.response != null ? JSON.stringify(parsedWrapUp.response, null, 2) : '';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
When the model returns a non-string (array, object), it is JSON-stringified with 2-space indentation. The user gets a readable representation of the intended content rather than a crash. This preserves the model's intent while enforcing the string contract.
|
|
119
|
+
|
|
120
|
+
### 2. `src/channels/telegram/index.js` — defense-in-depth type guard
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
// Before:
|
|
124
|
+
const text = result.response?.trim()
|
|
125
|
+
|| 'The agent encountered an error and could not produce a response. Please try again.';
|
|
126
|
+
|
|
127
|
+
// After:
|
|
128
|
+
const rawResponse = typeof result.response === 'string'
|
|
129
|
+
? result.response
|
|
130
|
+
: result.response != null ? JSON.stringify(result.response, null, 2) : '';
|
|
131
|
+
const text = rawResponse.trim()
|
|
132
|
+
|| 'The agent encountered an error and could not produce a response. Please try again.';
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 3. `docs/system-prompt.md` — explicit type constraint on `response`
|
|
136
|
+
|
|
137
|
+
Added one sentence to the `## Response Format` section:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
The `response` value must be a plain text string — never an array or object. If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Outcome
|
|
146
|
+
|
|
147
|
+
| Fix | Files changed |
|
|
148
|
+
|-----|--------------|
|
|
149
|
+
| Coerce `parsed.response` to string in main and wrap-up paths | `src/server/agent.js` |
|
|
150
|
+
| Type guard before `.trim()` call | `src/channels/telegram/index.js` |
|
|
151
|
+
| Explicit type constraint on `response` field | `docs/system-prompt.md` |
|
|
152
|
+
|
|
153
|
+
**Effect on the debugging session**: instead of "Sorry, something went wrong sending the response", the user would have received the tool call list formatted as a readable JSON string.
|
package/docs/system-prompt.md
CHANGED
|
@@ -30,6 +30,8 @@ There are two types of responses depending on whether you need to use tools:
|
|
|
30
30
|
"logSummary": "A concise explanation of what you did and why, written for a human reading the logs."
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
The `response` value must be a plain text string — never an array or object. If you need to present structured data (e.g. a list of items), format it as text within the string value.
|
|
34
|
+
|
|
33
35
|
Never include markdown code fences, preamble, or any text outside this JSON object. If you cannot complete a task, explain why in the `response` field — still as valid JSON.
|
|
34
36
|
|
|
35
37
|
## Tool Use
|
|
@@ -54,6 +56,21 @@ The `exec` tool runs real shell commands on the server. Use it responsibly:
|
|
|
54
56
|
- **Avoid commands with unbounded runtime.** If a command could run indefinitely or scan an unknown-size tree, scope it first.
|
|
55
57
|
- **Writing multi-line files**: use `printf '...'` or a heredoc (`cat <<'EOF' > file`) instead of `echo -e`. The `-e` flag is not portable — on Ubuntu `/bin/sh` it is treated as literal text, corrupting the file.
|
|
56
58
|
|
|
59
|
+
## Execution Timeouts
|
|
60
|
+
|
|
61
|
+
Every tool call is wrapped in a server-side timeout that the tool's code cannot override:
|
|
62
|
+
|
|
63
|
+
- **`exec`** — 5-minute cap. Sufficient for scans, builds, and most long-running commands.
|
|
64
|
+
- **`system_install`** — 5-minute cap. Use for installing system binaries via a package manager.
|
|
65
|
+
- **Custom tools via `save_tool`** — default 60s unless you pass `timeout` (in ms, max 600000). If a custom tool wraps a slow operation, set `timeout` explicitly.
|
|
66
|
+
|
|
67
|
+
**For truly long-running processes (> 5 minutes)**: run in the background and poll for results:
|
|
68
|
+
```sh
|
|
69
|
+
nohup long-running-command > /tmp/output.log 2>&1 & echo $!
|
|
70
|
+
# Check progress later
|
|
71
|
+
cat /tmp/output.log
|
|
72
|
+
```
|
|
73
|
+
|
|
57
74
|
## Failure Recovery
|
|
58
75
|
|
|
59
76
|
When a tool or command fails:
|
|
@@ -71,6 +88,7 @@ When building a custom tool with `save_tool`:
|
|
|
71
88
|
- **Installing an npm package**: use the `npm_install` tool — it handles the correct install directory automatically. Then create the tool with `save_tool`. The tool code can `require('<package-name>')` directly.
|
|
72
89
|
- **Installing a system binary** (e.g. nuclei, jq, ffmpeg, git): use the `system_install` tool — never use exec for this. It auto-detects the available package manager (brew/apt-get/snap) and has a 5-minute timeout sized for real downloads.
|
|
73
90
|
- **Available bindings in tool code**: `args`, `fs`, `path`, `process`, `require`, `__jarvisDir` (absolute path to the jarvis server directory).
|
|
91
|
+
- **Long-running custom tools**: if your tool wraps an operation that takes more than 60 seconds (e.g. a network call, a slow computation), pass `timeout` in milliseconds to `save_tool` (max 600000 = 10 minutes). Example: `save_tool({ name: "run_scan", timeout: 300000, ... })`.
|
|
74
92
|
|
|
75
93
|
## logSummary Guidelines
|
|
76
94
|
|
package/package.json
CHANGED
|
@@ -67,8 +67,11 @@ export async function startTelegramChannel(config) {
|
|
|
67
67
|
|
|
68
68
|
try {
|
|
69
69
|
const MAX_TG = 4096;
|
|
70
|
-
// Guard against empty response (e.g.
|
|
71
|
-
const
|
|
70
|
+
// Guard against empty or non-string response (e.g. model returns array instead of string)
|
|
71
|
+
const rawResponse = typeof result.response === 'string'
|
|
72
|
+
? result.response
|
|
73
|
+
: result.response != null ? JSON.stringify(result.response, null, 2) : '';
|
|
74
|
+
const text = rawResponse.trim()
|
|
72
75
|
|| 'The agent encountered an error and could not produce a response. Please try again.';
|
|
73
76
|
if (text.length <= MAX_TG) {
|
|
74
77
|
await ctx.reply(text);
|
package/src/server/agent.js
CHANGED
|
@@ -247,7 +247,9 @@ async function runAgentLoop(client, config, session, prepareMessages) {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
session.messages.push({ role: 'assistant', content });
|
|
250
|
-
response = parsed.response
|
|
250
|
+
response = typeof parsed.response === 'string'
|
|
251
|
+
? parsed.response
|
|
252
|
+
: JSON.stringify(parsed.response, null, 2);
|
|
251
253
|
logSummary = parsed.logSummary || '';
|
|
252
254
|
|
|
253
255
|
done = true;
|
|
@@ -312,7 +314,9 @@ async function runAgentLoop(client, config, session, prepareMessages) {
|
|
|
312
314
|
session.messages.push({ role: 'assistant', content: wrapUpContent });
|
|
313
315
|
|
|
314
316
|
if (parsedWrapUp) {
|
|
315
|
-
response = parsedWrapUp.response
|
|
317
|
+
response = typeof parsedWrapUp.response === 'string'
|
|
318
|
+
? parsedWrapUp.response
|
|
319
|
+
: parsedWrapUp.response != null ? JSON.stringify(parsedWrapUp.response, null, 2) : '';
|
|
316
320
|
logSummary = parsedWrapUp.logSummary || '';
|
|
317
321
|
if (parsedWrapUp.checkpoint) {
|
|
318
322
|
return {
|
package/src/server/tools.js
CHANGED
|
@@ -43,6 +43,7 @@ const SEED_TOOLS = {
|
|
|
43
43
|
`,
|
|
44
44
|
},
|
|
45
45
|
exec: {
|
|
46
|
+
timeout: 300_000, // 5 minutes — scans, builds, and long commands need more than 60s
|
|
46
47
|
definition: {
|
|
47
48
|
type: 'function',
|
|
48
49
|
function: {
|
|
@@ -67,7 +68,7 @@ const SEED_TOOLS = {
|
|
|
67
68
|
try {
|
|
68
69
|
const { stdout, stderr } = await execAsync(args.cmd, {
|
|
69
70
|
encoding: "utf8",
|
|
70
|
-
timeout:
|
|
71
|
+
timeout: 270000, // 4.5 min — leaves headroom before the outer 5-min tool timeout
|
|
71
72
|
maxBuffer: 2 * 1024 * 1024,
|
|
72
73
|
});
|
|
73
74
|
return { status: "ok", exitCode: 0, stdout, stderr };
|
|
@@ -144,12 +145,16 @@ const SEED_TOOLS = {
|
|
|
144
145
|
type: 'string',
|
|
145
146
|
description: 'The body of an async function. Must end with a return statement — the returned value becomes the tool result. Available bindings: args (your tool parameters), fs (node:fs), path (node:path), process, require, __jarvisDir (absolute path to the jarvis server directory — use path.resolve(__jarvisDir, "../..") to get the project root for npm installs). Do NOT wrap in a function declaration. Example: const raw = await fs.promises.readFile(args.filePath, "utf8"); const data = JSON.parse(raw); return { count: data.length, first: data[0] };',
|
|
146
147
|
},
|
|
148
|
+
timeout: {
|
|
149
|
+
type: 'number',
|
|
150
|
+
description: 'Optional execution timeout in milliseconds for this tool (max 600000 = 10 minutes). Use this when the tool wraps a slow operation (e.g. a network request or long computation) that exceeds the default 60-second limit. If omitted, the default 60-second timeout applies.',
|
|
151
|
+
},
|
|
147
152
|
},
|
|
148
153
|
required: ['name', 'description', 'parameters', 'code'],
|
|
149
154
|
},
|
|
150
155
|
},
|
|
151
156
|
},
|
|
152
|
-
code: `const toolsFile = path.join(process.env.HOME, '.jarvis/data/tools/tools.json'); const raw = await fs.promises.readFile(toolsFile, 'utf8').catch(() => '{}'); const tools = JSON.parse(raw); let parameters = args.parameters; if (typeof parameters === 'string') { try { parameters = JSON.parse(parameters); } catch { return { status: 'error', error: 'parameters must be a JSON Schema object, not a string. Pass the object directly, not as a JSON-serialized string.' }; } } if (typeof parameters !== 'object' || parameters === null || Array.isArray(parameters)) { return { status: 'error', error: 'parameters must be a JSON Schema object (e.g. { type: "object", properties: {...} }).' }; }
|
|
157
|
+
code: `const toolsFile = path.join(process.env.HOME, '.jarvis/data/tools/tools.json'); const raw = await fs.promises.readFile(toolsFile, 'utf8').catch(() => '{}'); const tools = JSON.parse(raw); let parameters = args.parameters; if (typeof parameters === 'string') { try { parameters = JSON.parse(parameters); } catch { return { status: 'error', error: 'parameters must be a JSON Schema object, not a string. Pass the object directly, not as a JSON-serialized string.' }; } } if (typeof parameters !== 'object' || parameters === null || Array.isArray(parameters)) { return { status: 'error', error: 'parameters must be a JSON Schema object (e.g. { type: "object", properties: {...} }).' }; } const entry = { definition: { type: 'function', function: { name: args.name, description: args.description, parameters } }, code: args.code }; if (args.timeout !== undefined) { const t = Number(args.timeout); if (!Number.isFinite(t) || t <= 0) return { status: 'error', error: 'timeout must be a positive number in milliseconds.' }; entry.timeout = Math.min(t, 600_000); } tools[args.name] = entry; await fs.promises.writeFile(toolsFile, JSON.stringify(tools, null, 2), 'utf8'); return { status: 'ok', saved: args.name, timeout: entry.timeout || 60000 };`,
|
|
153
158
|
},
|
|
154
159
|
get_tool: {
|
|
155
160
|
definition: {
|