micro-models-agent 0.8.0 → 0.10.0
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/config/config.js +66 -3
- package/dist/config/security.js +1 -1
- package/dist/core/agent.js +5 -0
- package/dist/core/bootstrap.js +10 -0
- package/dist/i18n/en.json +18 -0
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/ru.json +18 -0
- package/dist/main.js +16339 -0
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +3 -0
- package/dist/modules/processes/registry.js +142 -0
- package/dist/modules/processes/runner.js +109 -0
- package/dist/modules/security/audit-log.js +8 -0
- package/dist/modules/security/command-validator.js +22 -9
- package/dist/modules/security/encryption.js +22 -2
- package/dist/tools/bash.js +49 -19
- package/dist/tools/executor.js +10 -4
- package/dist/tools/grep-tool.js +24 -18
- package/dist/tools/index.js +5 -1
- package/dist/tools/pipeline-run.js +114 -9
- package/dist/tools/process-kill.js +29 -0
- package/dist/tools/process-list.js +38 -0
- package/dist/tools/process-log.js +39 -0
- package/dist/tools/read-file.js +6 -1
- package/dist/tools/subagent.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic detection of long-running (server/watch) commands.
|
|
3
|
+
*
|
|
4
|
+
* The explicit `background: true` bash parameter always wins; this list is a
|
|
5
|
+
* convenience so dev servers don't block the agent. It only inspects the raw
|
|
6
|
+
* shell command (a technical process-type decision, not task classification).
|
|
7
|
+
*/
|
|
8
|
+
const LONG_RUNNING_PATTERNS = [
|
|
9
|
+
/\b(npm|pnpm|yarn|bun|npx)\s+(run\s+)?(dev|start|serve|preview|watch|server)\b/i,
|
|
10
|
+
/\b(deno|node)\s+.*\b(run\s+)?(dev|serve|watch|server)\b/i,
|
|
11
|
+
/(^|\s)--watch\b/i,
|
|
12
|
+
/(^|\s)-w\b/i,
|
|
13
|
+
/\bnodemon\b/i,
|
|
14
|
+
/\btsx watch\b/i,
|
|
15
|
+
/\b(ts-node|tsc)\s+.*--watch\b/i,
|
|
16
|
+
/\bvite(?!\s+(build|create))\b/i,
|
|
17
|
+
/\bwebpack(-dev-server|\s+serve)\b/i,
|
|
18
|
+
/\bastro dev\b/i,
|
|
19
|
+
/\bnext (dev|start)\b/i,
|
|
20
|
+
/\bnuxt (dev|start)\b/i,
|
|
21
|
+
/\bsvelte-kit (dev|preview)\b/i,
|
|
22
|
+
/\bgatsby develop\b/i,
|
|
23
|
+
/\bdocker(-compose)?\s+.*\bup\b/i,
|
|
24
|
+
/\buvicorn\b|\bgunicorn\b/i,
|
|
25
|
+
/\bpython\s+-m\s+http\.server\b/i,
|
|
26
|
+
/\bflask run\b/i,
|
|
27
|
+
/\bphp artisan serve\b/i,
|
|
28
|
+
/\brails (server|s)\b/i,
|
|
29
|
+
/\bvitest watch\b/i,
|
|
30
|
+
/\bjest --watch\b/i,
|
|
31
|
+
];
|
|
32
|
+
export function isLongRunningCommand(command) {
|
|
33
|
+
return LONG_RUNNING_PATTERNS.some((pattern) => pattern.test(command));
|
|
34
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
const MAX_LOG_LINES = 300;
|
|
4
|
+
const MAX_KEPT_PROCESSES = 20;
|
|
5
|
+
let seq = 0;
|
|
6
|
+
function killTree(child) {
|
|
7
|
+
const pid = child.pid;
|
|
8
|
+
if (!pid)
|
|
9
|
+
return;
|
|
10
|
+
if (platform() === "win32") {
|
|
11
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
12
|
+
windowsHide: true,
|
|
13
|
+
stdio: "ignore",
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
process.kill(-pid, "SIGTERM");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
try {
|
|
22
|
+
child.kill("SIGKILL");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* already dead */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class ProcessRegistry {
|
|
30
|
+
procs = new Map();
|
|
31
|
+
children = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Start a background process. Returns immediately with the entry; output is
|
|
34
|
+
* buffered into `entry.log` as it arrives.
|
|
35
|
+
*/
|
|
36
|
+
start(command, cwd, sessionId) {
|
|
37
|
+
const id = `proc_${Date.now()}_${++seq}`;
|
|
38
|
+
const entry = {
|
|
39
|
+
id,
|
|
40
|
+
pid: 0,
|
|
41
|
+
command,
|
|
42
|
+
cwd,
|
|
43
|
+
sessionId,
|
|
44
|
+
status: "running",
|
|
45
|
+
exitCode: null,
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
log: [],
|
|
48
|
+
};
|
|
49
|
+
this.trimOldEntries();
|
|
50
|
+
this.procs.set(id, entry);
|
|
51
|
+
const child = spawn(command, {
|
|
52
|
+
cwd,
|
|
53
|
+
shell: true,
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
detached: platform() !== "win32",
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
this.children.set(id, child);
|
|
59
|
+
entry.pid = child.pid ?? 0;
|
|
60
|
+
let partial = "";
|
|
61
|
+
const append = (chunk) => {
|
|
62
|
+
const text = partial + chunk.toString();
|
|
63
|
+
const lines = text.split(/\r?\n/);
|
|
64
|
+
partial = lines.pop() ?? "";
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (entry.log.length >= MAX_LOG_LINES) {
|
|
67
|
+
entry.log.shift();
|
|
68
|
+
}
|
|
69
|
+
entry.log.push(line);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
child.stdout?.on("data", append);
|
|
73
|
+
child.stderr?.on("data", append);
|
|
74
|
+
child.on("error", (err) => {
|
|
75
|
+
if (entry.status === "running") {
|
|
76
|
+
entry.status = "killed";
|
|
77
|
+
}
|
|
78
|
+
append(Buffer.from(`[process error] ${err.message}`));
|
|
79
|
+
});
|
|
80
|
+
child.on("close", (code) => {
|
|
81
|
+
if (entry.status === "running") {
|
|
82
|
+
entry.status = "exited";
|
|
83
|
+
}
|
|
84
|
+
entry.exitCode = code;
|
|
85
|
+
this.children.delete(id);
|
|
86
|
+
if (partial) {
|
|
87
|
+
entry.log.push(partial);
|
|
88
|
+
partial = "";
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
list(sessionId) {
|
|
94
|
+
const all = Array.from(this.procs.values());
|
|
95
|
+
if (!sessionId)
|
|
96
|
+
return all;
|
|
97
|
+
return all.filter((p) => p.sessionId === sessionId);
|
|
98
|
+
}
|
|
99
|
+
get(id) {
|
|
100
|
+
return this.procs.get(id);
|
|
101
|
+
}
|
|
102
|
+
getLog(id, tail) {
|
|
103
|
+
const entry = this.procs.get(id);
|
|
104
|
+
if (!entry)
|
|
105
|
+
return "";
|
|
106
|
+
const lines = tail && tail > 0 ? entry.log.slice(-tail) : entry.log;
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
/** Kill a background process tree. Returns false when not found. */
|
|
110
|
+
kill(id) {
|
|
111
|
+
const entry = this.procs.get(id);
|
|
112
|
+
if (!entry)
|
|
113
|
+
return false;
|
|
114
|
+
if (entry.status === "running") {
|
|
115
|
+
entry.status = "killed";
|
|
116
|
+
const child = this.children.get(id);
|
|
117
|
+
if (child) {
|
|
118
|
+
killTree(child);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/** Kill every managed background process (used on agent shutdown). */
|
|
124
|
+
killAll(sessionId) {
|
|
125
|
+
const targets = this.list(sessionId);
|
|
126
|
+
for (const entry of targets) {
|
|
127
|
+
this.kill(entry.id);
|
|
128
|
+
}
|
|
129
|
+
return targets.length;
|
|
130
|
+
}
|
|
131
|
+
trimOldEntries() {
|
|
132
|
+
if (this.procs.size < MAX_KEPT_PROCESSES)
|
|
133
|
+
return;
|
|
134
|
+
const sorted = Array.from(this.procs.values()).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
135
|
+
const toRemove = sorted.slice(0, sorted.length - MAX_KEPT_PROCESSES + 1);
|
|
136
|
+
for (const entry of toRemove) {
|
|
137
|
+
this.procs.delete(entry.id);
|
|
138
|
+
this.children.delete(entry.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export const processRegistry = new ProcessRegistry();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
const activeChildren = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Kill a foreground command previously registered by `callId` (used by the
|
|
6
|
+
* tool executor when the execution timeout fires). Returns true when a child
|
|
7
|
+
* was found and killed.
|
|
8
|
+
*/
|
|
9
|
+
export function killByCallId(callId) {
|
|
10
|
+
const entry = activeChildren.get(callId);
|
|
11
|
+
if (!entry)
|
|
12
|
+
return false;
|
|
13
|
+
activeChildren.delete(callId);
|
|
14
|
+
try {
|
|
15
|
+
entry.kill();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* already dead */
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function killTree(child) {
|
|
23
|
+
const pid = child.pid;
|
|
24
|
+
if (!pid)
|
|
25
|
+
return;
|
|
26
|
+
if (platform() === "win32") {
|
|
27
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
// `detached: true` makes the child a process-group leader on POSIX,
|
|
35
|
+
// so killing -pid terminates the whole tree.
|
|
36
|
+
process.kill(-pid, "SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
try {
|
|
40
|
+
child.kill("SIGKILL");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* already dead */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Run a shell command asynchronously (non-blocking event loop) and collect
|
|
49
|
+
* stdout/stderr. The child is killed on timeout and can be killed externally
|
|
50
|
+
* via `killByCallId` (used by the tool executor timeout).
|
|
51
|
+
*/
|
|
52
|
+
export function runCommand(command, options = {}) {
|
|
53
|
+
const { cwd, callId, timeoutMs = 120_000, maxBuffer = 10 * 1024 * 1024, } = options;
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let timedOut = false;
|
|
56
|
+
let stdout = "";
|
|
57
|
+
let stderr = "";
|
|
58
|
+
const child = spawn(command, {
|
|
59
|
+
cwd,
|
|
60
|
+
shell: true,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
detached: platform() !== "win32",
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
});
|
|
65
|
+
const append = (ref, chunk) => {
|
|
66
|
+
const text = chunk.toString();
|
|
67
|
+
const next = ref.value.length + text.length;
|
|
68
|
+
if (next > maxBuffer) {
|
|
69
|
+
ref.value =
|
|
70
|
+
ref.value.slice(ref.value.length + text.length - maxBuffer) + text;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
ref.value = ref.value + text;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const stdoutRef = { value: stdout };
|
|
77
|
+
const stderrRef = { value: stderr };
|
|
78
|
+
child.stdout?.on("data", (chunk) => append(stdoutRef, chunk));
|
|
79
|
+
child.stderr?.on("data", (chunk) => append(stderrRef, chunk));
|
|
80
|
+
const entry = {
|
|
81
|
+
kill: () => killTree(child),
|
|
82
|
+
};
|
|
83
|
+
if (callId) {
|
|
84
|
+
activeChildren.set(callId, entry);
|
|
85
|
+
}
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
killTree(child);
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
child.on("error", () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
if (callId)
|
|
93
|
+
activeChildren.delete(callId);
|
|
94
|
+
resolve({
|
|
95
|
+
stdout: stdoutRef.value,
|
|
96
|
+
stderr: stderrRef.value,
|
|
97
|
+
code: null,
|
|
98
|
+
signal: null,
|
|
99
|
+
timedOut,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
child.on("close", (code, signal) => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
if (callId)
|
|
105
|
+
activeChildren.delete(callId);
|
|
106
|
+
resolve({ stdout: stdoutRef.value, stderr: stderrRef.value, code, signal, timedOut });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, appendFileSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
+
import { globalAuditNotifier } from "./audit-notifier";
|
|
4
5
|
/**
|
|
5
6
|
* Directory and file path for audit logs
|
|
6
7
|
*/
|
|
@@ -27,6 +28,13 @@ export function logAudit(entry) {
|
|
|
27
28
|
// Silently fail if we can't write to audit log
|
|
28
29
|
// This shouldn't break the agent
|
|
29
30
|
}
|
|
31
|
+
// Notify audit notifier if enabled
|
|
32
|
+
try {
|
|
33
|
+
globalAuditNotifier.notify(entry);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Silently fail if notification fails
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Log a tool call
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { DEFAULT_SECURITY_CONFIG } from "../../config/security";
|
|
2
|
+
// Fallback in case DEFAULT_SECURITY_CONFIG is not available
|
|
3
|
+
const FALLBACK_BASH_CONFIG = {
|
|
4
|
+
blacklist: ['rm', 'dd', 'chmod', 'wget', 'curl', 'scp', 'ssh', 'nc', 'netcat'],
|
|
5
|
+
whitelist: [],
|
|
6
|
+
blockDangerousFlags: true,
|
|
7
|
+
dangerousFlags: ['--force', '-rf', '--no-preserve-root'],
|
|
8
|
+
dangerousOperators: ['>', '>>', '2>', '2>>', '|', '&&', '||', ';', '&', '`'],
|
|
9
|
+
logCommands: true,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_BASH_CONFIG = DEFAULT_SECURITY_CONFIG?.bash || FALLBACK_BASH_CONFIG;
|
|
1
12
|
/**
|
|
2
13
|
* Check if a command is allowed based on security configuration
|
|
3
14
|
*/
|
|
4
15
|
export function isCommandAllowed(command, securityConfig) {
|
|
5
16
|
// If no security config, allow everything (backward compatibility)
|
|
6
|
-
|
|
7
|
-
|
|
17
|
+
let config = securityConfig || DEFAULT_BASH_CONFIG;
|
|
18
|
+
// Ensure config has all required fields
|
|
19
|
+
if (!config.blacklist || !Array.isArray(config.blacklist)) {
|
|
20
|
+
config = FALLBACK_BASH_CONFIG;
|
|
8
21
|
}
|
|
9
22
|
const trimmedCommand = command.trim();
|
|
10
23
|
if (!trimmedCommand) {
|
|
@@ -13,8 +26,8 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
13
26
|
// Extract base command (first word)
|
|
14
27
|
const baseCommand = trimmedCommand.split(/\s+/)[0];
|
|
15
28
|
// Check whitelist first (if non-empty, only whitelisted commands are allowed)
|
|
16
|
-
if (
|
|
17
|
-
if (!
|
|
29
|
+
if (config.whitelist.length > 0) {
|
|
30
|
+
if (!config.whitelist.includes(baseCommand)) {
|
|
18
31
|
return {
|
|
19
32
|
allowed: false,
|
|
20
33
|
reason: `Command "${baseCommand}" is not in the whitelist`,
|
|
@@ -22,15 +35,15 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
// Check blacklist
|
|
25
|
-
if (
|
|
38
|
+
if (config.blacklist.includes(baseCommand)) {
|
|
26
39
|
return {
|
|
27
40
|
allowed: false,
|
|
28
41
|
reason: `Command "${baseCommand}" is blacklisted`,
|
|
29
42
|
};
|
|
30
43
|
}
|
|
31
44
|
// Check for dangerous operators
|
|
32
|
-
if (
|
|
33
|
-
for (const op of
|
|
45
|
+
if (config.blockDangerousFlags) {
|
|
46
|
+
for (const op of config.dangerousOperators || []) {
|
|
34
47
|
if (command.includes(op)) {
|
|
35
48
|
return {
|
|
36
49
|
allowed: false,
|
|
@@ -40,8 +53,8 @@ export function isCommandAllowed(command, securityConfig) {
|
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
// Check for dangerous flags
|
|
43
|
-
if (
|
|
44
|
-
for (const flag of
|
|
56
|
+
if (config.blockDangerousFlags) {
|
|
57
|
+
for (const flag of config.dangerousFlags || []) {
|
|
45
58
|
// Check for flag as whole word or with equals
|
|
46
59
|
const flagPattern = new RegExp(`(?:^|\\s)${flag}(?:\\s|$|=)`);
|
|
47
60
|
if (flagPattern.test(command)) {
|
|
@@ -166,7 +166,17 @@ export function encryptSensitiveFields(obj, key) {
|
|
|
166
166
|
result[fieldName] = encryptString(value, key);
|
|
167
167
|
}
|
|
168
168
|
else if (typeof value === 'object' && value !== null) {
|
|
169
|
-
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
result[fieldName] = value.map(item => typeof item === 'object' && item !== null && !(item instanceof RegExp)
|
|
171
|
+
? encryptSensitiveFields(item, key)
|
|
172
|
+
: item);
|
|
173
|
+
}
|
|
174
|
+
else if (value instanceof RegExp) {
|
|
175
|
+
result[fieldName] = value;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
result[fieldName] = encryptSensitiveFields(value, key);
|
|
179
|
+
}
|
|
170
180
|
}
|
|
171
181
|
else {
|
|
172
182
|
result[fieldName] = value;
|
|
@@ -190,7 +200,17 @@ export function decryptSensitiveFields(obj, key) {
|
|
|
190
200
|
}
|
|
191
201
|
}
|
|
192
202
|
else if (typeof value === 'object' && value !== null) {
|
|
193
|
-
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
result[fieldName] = value.map(item => typeof item === 'object' && item !== null && !(item instanceof RegExp)
|
|
205
|
+
? decryptSensitiveFields(item, key)
|
|
206
|
+
: item);
|
|
207
|
+
}
|
|
208
|
+
else if (value instanceof RegExp) {
|
|
209
|
+
result[fieldName] = value;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
result[fieldName] = decryptSensitiveFields(value, key);
|
|
213
|
+
}
|
|
194
214
|
}
|
|
195
215
|
else {
|
|
196
216
|
result[fieldName] = value;
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { platform } from 'os';
|
|
3
1
|
import { isCommandAllowed, sanitizeCommandForLog } from '../modules/security/command-validator';
|
|
4
2
|
import { logBashCommand, logSecurityBlock } from '../modules/security/audit-log';
|
|
5
3
|
import { getSessionSecurityConfig } from '../modules/security/session-isolation';
|
|
4
|
+
import { DEFAULT_SECURITY_CONFIG } from '../config/security';
|
|
5
|
+
import { runCommand, processRegistry, isLongRunningCommand } from '../modules/processes';
|
|
6
|
+
import { t } from '../i18n/index';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
const BASH_TIMEOUT_MS = 120_000;
|
|
6
9
|
function adaptCommandForWindows(command) {
|
|
7
10
|
if (platform() !== 'win32')
|
|
8
11
|
return command;
|
|
@@ -18,13 +21,14 @@ function adaptCommandForWindows(command) {
|
|
|
18
21
|
}
|
|
19
22
|
export const bashTool = {
|
|
20
23
|
name: 'bash',
|
|
21
|
-
description: 'Execute a shell command and return its output. Use for running tests, build, git, and shell operations.',
|
|
24
|
+
description: 'Execute a shell command and return its output. Use for running tests, build, git, and shell operations. Long-running commands (dev servers, watchers) start in the background and return a process id immediately — manage them with process_list, process_log, process_kill. Set background=true to force background execution.',
|
|
22
25
|
tags: ['shell', 'code'],
|
|
23
26
|
parameters: {
|
|
24
27
|
type: 'object',
|
|
25
28
|
properties: {
|
|
26
29
|
command: { type: 'string', description: 'Shell command to execute' },
|
|
27
30
|
workdir: { type: 'string', description: 'Working directory (default: baseDir)' },
|
|
31
|
+
background: { type: 'boolean', description: 'Start the command in the background and return immediately with a process id (default: auto-detect long-running commands)' },
|
|
28
32
|
},
|
|
29
33
|
required: ['command'],
|
|
30
34
|
},
|
|
@@ -32,10 +36,12 @@ export const bashTool = {
|
|
|
32
36
|
const originalCommand = String(args.command);
|
|
33
37
|
const command = adaptCommandForWindows(originalCommand);
|
|
34
38
|
const workdir = args.workdir ? String(args.workdir) : ctx.baseDir;
|
|
35
|
-
// Get session-specific security config
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
// Get session-specific security config with defaults
|
|
40
|
+
const appConfig = ctx.config || {};
|
|
41
|
+
const fullSecurityConfig = ctx.sessionContext
|
|
42
|
+
? getSessionSecurityConfig(appConfig, ctx.sessionContext)
|
|
43
|
+
: appConfig.security || DEFAULT_SECURITY_CONFIG;
|
|
44
|
+
const securityConfig = fullSecurityConfig.bash || DEFAULT_SECURITY_CONFIG.bash;
|
|
39
45
|
const validation = isCommandAllowed(command, securityConfig);
|
|
40
46
|
if (!validation.allowed) {
|
|
41
47
|
// Log security block
|
|
@@ -50,28 +56,52 @@ export const bashTool = {
|
|
|
50
56
|
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, // Will be updated after execution
|
|
51
57
|
`Working directory: ${workdir}`);
|
|
52
58
|
}
|
|
59
|
+
const background = args.background === true || isLongRunningCommand(command);
|
|
60
|
+
if (background) {
|
|
61
|
+
const entry = processRegistry.start(command, workdir, ctx.sessionId);
|
|
62
|
+
if (securityConfig?.logCommands) {
|
|
63
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), true, `Started in background: ${entry.id} (PID ${entry.pid})`);
|
|
64
|
+
}
|
|
65
|
+
const explicit = args.background === true;
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
output: `${t("proc.started", {
|
|
69
|
+
id: entry.id,
|
|
70
|
+
pid: entry.pid,
|
|
71
|
+
command,
|
|
72
|
+
})}${explicit ? "" : `\n${t("proc.detected_hint")}`}\n${t("proc.manage_hint", {
|
|
73
|
+
id: entry.id,
|
|
74
|
+
})}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
53
77
|
try {
|
|
54
|
-
const
|
|
78
|
+
const res = await runCommand(command, {
|
|
55
79
|
cwd: workdir,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
timeout: 120_000,
|
|
80
|
+
callId: ctx.activeCallId,
|
|
81
|
+
timeoutMs: BASH_TIMEOUT_MS,
|
|
59
82
|
});
|
|
60
|
-
|
|
83
|
+
const parts = [res.stdout.trimEnd(), res.stderr.trimEnd()].filter(Boolean);
|
|
84
|
+
let output = parts.join("\n");
|
|
85
|
+
if (res.timedOut) {
|
|
86
|
+
output = `${output ? output + "\n" : ""}${t("proc.timed_out", {
|
|
87
|
+
ms: BASH_TIMEOUT_MS,
|
|
88
|
+
})}`;
|
|
89
|
+
}
|
|
90
|
+
else if (!output && res.code !== 0) {
|
|
91
|
+
output = `(exit code ${res.code})`;
|
|
92
|
+
}
|
|
93
|
+
// Update audit log with result
|
|
61
94
|
if (securityConfig?.logCommands) {
|
|
62
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command),
|
|
95
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), res.code === 0, `Working directory: ${workdir}, Output length: ${res.stdout.length}`);
|
|
63
96
|
}
|
|
64
|
-
return { success:
|
|
97
|
+
return { success: res.code === 0, output };
|
|
65
98
|
}
|
|
66
99
|
catch (e) {
|
|
67
|
-
const stderr = e.stderr?.toString() || '';
|
|
68
|
-
const stdout = e.stdout?.toString() || '';
|
|
69
|
-
const errorOutput = stderr || stdout || e.message;
|
|
70
100
|
// Update audit log with failure
|
|
71
101
|
if (securityConfig?.logCommands) {
|
|
72
|
-
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${
|
|
102
|
+
logBashCommand(ctx.sessionId, sanitizeCommandForLog(command), false, `Working directory: ${workdir}, Error: ${e.message?.slice(0, 100) || ""}`);
|
|
73
103
|
}
|
|
74
|
-
return { success: false, output:
|
|
104
|
+
return { success: false, output: e.message || String(e) };
|
|
75
105
|
}
|
|
76
106
|
},
|
|
77
107
|
};
|
package/dist/tools/executor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { killByCallId } from "../modules/processes/runner";
|
|
1
2
|
import { t } from "../i18n/index";
|
|
2
3
|
const TOOL_EXECUTION_TIMEOUT_MS = 60000;
|
|
3
4
|
export class ToolExecutor {
|
|
@@ -39,16 +40,21 @@ export class ToolExecutor {
|
|
|
39
40
|
}
|
|
40
41
|
let result;
|
|
41
42
|
try {
|
|
43
|
+
this.ctx.activeCallId = call.id;
|
|
42
44
|
if (tool.interactive) {
|
|
43
45
|
// Interactive tools wait for user input — no timeout.
|
|
44
46
|
result = await tool.handler(this.ctx, call.arguments);
|
|
45
47
|
}
|
|
46
48
|
else {
|
|
47
49
|
const timeoutPromise = new Promise((_, reject) => {
|
|
48
|
-
setTimeout(() =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
// Kill any long-running child process registered for this call.
|
|
52
|
+
killByCallId(call.id);
|
|
53
|
+
reject(new Error(t("tool.timeout", {
|
|
54
|
+
name: call.name,
|
|
55
|
+
seconds: TOOL_EXECUTION_TIMEOUT_MS / 1000,
|
|
56
|
+
})));
|
|
57
|
+
}, TOOL_EXECUTION_TIMEOUT_MS);
|
|
52
58
|
});
|
|
53
59
|
result = await Promise.race([
|
|
54
60
|
tool.handler(this.ctx, call.arguments),
|
package/dist/tools/grep-tool.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execFileSync, execSync } from 'child_process';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { t } from '../i18n/index';
|
|
4
|
-
import {
|
|
5
|
-
import { logSecurityBlock } from '../modules/security/audit-log';
|
|
4
|
+
import { logBashCommand } from '../modules/security/audit-log';
|
|
6
5
|
export const grepTool = {
|
|
7
6
|
name: 'grep',
|
|
8
7
|
description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
|
|
@@ -19,23 +18,16 @@ export const grepTool = {
|
|
|
19
18
|
handler: async (ctx, args) => {
|
|
20
19
|
const pattern = String(args.pattern);
|
|
21
20
|
const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
|
|
22
|
-
// Build
|
|
23
|
-
|
|
21
|
+
// Build rg arguments as an array to avoid shell interpretation of regex
|
|
22
|
+
// metacharacters like |, (, ) — these are regex patterns, not shell operators.
|
|
23
|
+
const rgArgs = ['-n', '--with-filename', pattern, searchPath];
|
|
24
24
|
if (args.include) {
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
// Security check: validate command (grep/rg should be allowed)
|
|
28
|
-
const securityConfig = ctx.config.security?.bash;
|
|
29
|
-
const validation = isCommandAllowed(cmd, securityConfig);
|
|
30
|
-
if (!validation.allowed) {
|
|
31
|
-
logSecurityBlock(ctx.sessionId, "bash_command", validation.reason || "Command blocked by security policy", sanitizeCommandForLog(cmd));
|
|
32
|
-
return {
|
|
33
|
-
success: false,
|
|
34
|
-
output: `[SECURITY BLOCKED] Command is not allowed: ${validation.reason}`,
|
|
35
|
-
};
|
|
25
|
+
rgArgs.push('-g', String(args.include));
|
|
36
26
|
}
|
|
27
|
+
// Log the search (sanitized) for audit purposes
|
|
28
|
+
logBashCommand(ctx.sessionId, `rg ${rgArgs.join(' ')}`, false, 'grep-tool');
|
|
37
29
|
try {
|
|
38
|
-
const output =
|
|
30
|
+
const output = execFileSync('rg', rgArgs, {
|
|
39
31
|
encoding: 'utf-8',
|
|
40
32
|
maxBuffer: 1024 * 1024,
|
|
41
33
|
cwd: ctx.baseDir,
|
|
@@ -45,7 +37,21 @@ export const grepTool = {
|
|
|
45
37
|
catch (e) {
|
|
46
38
|
if (e.status === 1)
|
|
47
39
|
return { success: true, output: t('file.no_matches') };
|
|
48
|
-
|
|
40
|
+
// Fall back to plain grep -r when rg is not available or fails
|
|
41
|
+
try {
|
|
42
|
+
const cmd = `grep -rn "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
|
|
43
|
+
const output = execSync(cmd, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
maxBuffer: 1024 * 1024,
|
|
46
|
+
cwd: ctx.baseDir,
|
|
47
|
+
});
|
|
48
|
+
return { success: true, output: output || t('file.no_matches') };
|
|
49
|
+
}
|
|
50
|
+
catch (e2) {
|
|
51
|
+
if (e2.status === 1)
|
|
52
|
+
return { success: true, output: t('file.no_matches') };
|
|
53
|
+
return { success: false, output: t('error.grep_failed', { message: e2.message }) };
|
|
54
|
+
}
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
};
|
package/dist/tools/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import { deleteFileTool } from './delete-file';
|
|
|
11
11
|
import { moveFileTool } from './move-file';
|
|
12
12
|
import { fileInfoTool } from './file-info';
|
|
13
13
|
import { bashTool } from './bash';
|
|
14
|
+
import { processListTool } from './process-list';
|
|
15
|
+
import { processLogTool } from './process-log';
|
|
16
|
+
import { processKillTool } from './process-kill';
|
|
14
17
|
import { subagentTool } from './subagent';
|
|
15
18
|
import { webSearchTool } from './web-search';
|
|
16
19
|
import { webFetchTool } from './web-fetch';
|
|
@@ -24,13 +27,14 @@ import { searchHistoryTool } from './search-history';
|
|
|
24
27
|
import { createBrowserTool } from './browser';
|
|
25
28
|
export { ToolRegistry, ToolExecutor };
|
|
26
29
|
export { filterToolsByTags } from './filter-tools';
|
|
27
|
-
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, webSearchTool, webFetchTool, webBrowseTool, questionTool, approveTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, createBrowserTool, };
|
|
30
|
+
export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, questionTool, approveTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, createBrowserTool, };
|
|
28
31
|
export function registerAllTools(registry, skillsModule) {
|
|
29
32
|
const tools = [
|
|
30
33
|
readFileTool, writeFileTool, editFileTool,
|
|
31
34
|
globTool, grepTool, listDirTool, createDirTool,
|
|
32
35
|
deleteFileTool, moveFileTool, fileInfoTool,
|
|
33
36
|
bashTool, subagentTool,
|
|
37
|
+
processListTool, processLogTool, processKillTool,
|
|
34
38
|
webSearchTool, webFetchTool, webBrowseTool,
|
|
35
39
|
questionTool, approveTool,
|
|
36
40
|
pipelineRunTool, mcpCallTool, searchHistoryTool,
|