agen-vektor 0.3.19 → 0.3.20
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/dist/tools/shell.js +63 -8
- package/package.json +1 -1
package/dist/tools/shell.js
CHANGED
|
@@ -7,39 +7,94 @@ exports.createShellTool = createShellTool;
|
|
|
7
7
|
* Every command is classified by the command policy and gated by permissions.
|
|
8
8
|
*/
|
|
9
9
|
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
// After the shell itself exits, wait this long for the stdio pipes to drain
|
|
11
|
+
// (normal commands flush and close within milliseconds) before giving up.
|
|
12
|
+
// A command that ORPHANS a child holding the pipes (background job, server,
|
|
13
|
+
// watcher …) would otherwise keep the tool — and the whole agent loop —
|
|
14
|
+
// hanging until the grandchild dies ("sering nyangkut di shell", 2026-09-10).
|
|
15
|
+
const ORPHAN_GRACE_MS = 500;
|
|
10
16
|
function runShell(command, cwd, opts = {}) {
|
|
11
17
|
return new Promise((resolve) => {
|
|
12
18
|
const timeoutMs = opts.timeoutMs || 120_000;
|
|
19
|
+
// Own process group (detached) → the timeout / orphan guard can kill the
|
|
20
|
+
// whole tree (shell AND grandchildren), same policy as background.ts.
|
|
13
21
|
const child = (0, node_child_process_1.spawn)(command, {
|
|
14
22
|
cwd,
|
|
15
23
|
shell: '/bin/sh',
|
|
16
24
|
env: { ...process.env, ...opts.env },
|
|
17
25
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
detached: true,
|
|
18
27
|
});
|
|
19
28
|
let stdout = '';
|
|
20
29
|
let stderr = '';
|
|
21
30
|
let timedOut = false;
|
|
31
|
+
let settled = false;
|
|
32
|
+
let graceTimer = null;
|
|
33
|
+
const killTree = () => {
|
|
34
|
+
if (child.pid != null) {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(-child.pid, 'SIGKILL'); // detached spawn ⇒ own process group
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* group already gone */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
child.kill('SIGKILL');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* already gone */
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const finish = (code) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
if (graceTimer)
|
|
55
|
+
clearTimeout(graceTimer);
|
|
56
|
+
resolve({ stdout, stderr, exitCode: code, timedOut });
|
|
57
|
+
};
|
|
22
58
|
const timer = setTimeout(() => {
|
|
23
59
|
timedOut = true;
|
|
24
|
-
child.kill(
|
|
60
|
+
killTree(); // was: child.kill() only — orphaned children kept the pipe open and the promise hanging past the timeout
|
|
61
|
+
// 'exit' fires once the shell dies and resolves immediately.
|
|
25
62
|
}, timeoutMs);
|
|
26
63
|
child.stdout.on('data', (d) => {
|
|
27
64
|
stdout += d.toString();
|
|
28
65
|
if (stdout.length > 2_000_000)
|
|
29
|
-
|
|
66
|
+
killTree();
|
|
30
67
|
});
|
|
31
68
|
child.stderr.on('data', (d) => {
|
|
32
69
|
stderr += d.toString();
|
|
33
70
|
if (stderr.length > 2_000_000)
|
|
34
|
-
|
|
71
|
+
killTree();
|
|
72
|
+
});
|
|
73
|
+
// 'exit' = the shell itself died; 'close' = shell AND stdio pipes closed.
|
|
74
|
+
// Resolve on whichever happens first with a small grace window after
|
|
75
|
+
// 'exit': a normal command flushes and closes well inside the window, so
|
|
76
|
+
// full output still arrives — but an orphaned grandchild holding the
|
|
77
|
+
// pipes can no longer hang the tool past it.
|
|
78
|
+
child.on('exit', (code) => {
|
|
79
|
+
if (timedOut) {
|
|
80
|
+
finish(code);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (graceTimer)
|
|
84
|
+
return;
|
|
85
|
+
graceTimer = setTimeout(() => {
|
|
86
|
+
killTree(); // stop the orphan so its output stops too
|
|
87
|
+
finish(code);
|
|
88
|
+
}, ORPHAN_GRACE_MS);
|
|
35
89
|
});
|
|
36
90
|
child.on('close', (code) => {
|
|
37
|
-
|
|
38
|
-
resolve({ stdout, stderr, exitCode: code, timedOut });
|
|
91
|
+
finish(code);
|
|
39
92
|
});
|
|
40
93
|
child.on('error', (e) => {
|
|
41
|
-
|
|
42
|
-
|
|
94
|
+
// set stderr BEFORE finish — the resolved object snapshots these strings
|
|
95
|
+
if (!stdout && !stderr)
|
|
96
|
+
stderr = `spawn error: ${e.message}`;
|
|
97
|
+
finish(-1);
|
|
43
98
|
});
|
|
44
99
|
});
|
|
45
100
|
}
|
|
@@ -52,7 +107,7 @@ function createShellTool() {
|
|
|
52
107
|
return {
|
|
53
108
|
definition: {
|
|
54
109
|
name: 'shell',
|
|
55
|
-
description: 'Run a shell command in the project directory. The command is classified for safety; risky commands require permission. Use for building, testing, installing, git, or any command execution. Long output is truncated. For git commits prefer the dedicated `git` tool and follow its commit-attribution guidance.',
|
|
110
|
+
description: 'Run a shell command in the project directory. The command is classified for safety; risky commands require permission. Use for building, testing, installing, git, or any command execution. Long output is truncated. Commands that keep running (dev servers, watchers, polling loops) must use run_in_background instead — a foreground shell is killed after its timeout. For git commits prefer the dedicated `git` tool and follow its commit-attribution guidance.',
|
|
56
111
|
parameters: {
|
|
57
112
|
type: 'object',
|
|
58
113
|
properties: {
|
package/package.json
CHANGED