183 lines
5.0 KiB
TypeScript
183 lines
5.0 KiB
TypeScript
|
|
import * as plugins from './plugins.js';
|
||
|
|
import * as interfaces from './smartagent.interfaces.js';
|
||
|
|
import { BaseToolWrapper } from './smartagent.tools.base.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shell tool for executing commands securely
|
||
|
|
* Wraps @push.rocks/smartshell with execSpawn for safety (no shell injection)
|
||
|
|
*/
|
||
|
|
export class ShellTool extends BaseToolWrapper {
|
||
|
|
public name = 'shell';
|
||
|
|
public description =
|
||
|
|
'Execute shell commands securely. Uses execSpawn (shell:false) to prevent command injection.';
|
||
|
|
|
||
|
|
public actions: interfaces.IToolAction[] = [
|
||
|
|
{
|
||
|
|
name: 'execute',
|
||
|
|
description:
|
||
|
|
'Execute a command with arguments (secure, no shell injection possible). Command and args are passed separately.',
|
||
|
|
parameters: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
command: {
|
||
|
|
type: 'string',
|
||
|
|
description: 'The command to execute (e.g., "ls", "cat", "grep", "node")',
|
||
|
|
},
|
||
|
|
args: {
|
||
|
|
type: 'array',
|
||
|
|
items: { type: 'string' },
|
||
|
|
description: 'Array of arguments (each argument is properly escaped)',
|
||
|
|
},
|
||
|
|
cwd: { type: 'string', description: 'Working directory for the command' },
|
||
|
|
timeout: { type: 'number', description: 'Timeout in milliseconds' },
|
||
|
|
env: {
|
||
|
|
type: 'object',
|
||
|
|
description: 'Additional environment variables (key-value pairs)',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
required: ['command'],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'which',
|
||
|
|
description: 'Check if a command exists and get its path',
|
||
|
|
parameters: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
command: { type: 'string', description: 'Command name to look up (e.g., "node", "git")' },
|
||
|
|
},
|
||
|
|
required: ['command'],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
private smartshell!: plugins.smartshell.Smartshell;
|
||
|
|
|
||
|
|
public async initialize(): Promise<void> {
|
||
|
|
this.smartshell = new plugins.smartshell.Smartshell({
|
||
|
|
executor: 'bash',
|
||
|
|
});
|
||
|
|
this.isInitialized = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async cleanup(): Promise<void> {
|
||
|
|
this.isInitialized = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async execute(
|
||
|
|
action: string,
|
||
|
|
params: Record<string, unknown>
|
||
|
|
): Promise<interfaces.IToolExecutionResult> {
|
||
|
|
this.validateAction(action);
|
||
|
|
this.ensureInitialized();
|
||
|
|
|
||
|
|
try {
|
||
|
|
switch (action) {
|
||
|
|
case 'execute': {
|
||
|
|
const command = params.command as string;
|
||
|
|
const args = (params.args as string[]) || [];
|
||
|
|
|
||
|
|
// Build options
|
||
|
|
const options: {
|
||
|
|
timeout?: number;
|
||
|
|
env?: NodeJS.ProcessEnv;
|
||
|
|
cwd?: string;
|
||
|
|
} = {};
|
||
|
|
|
||
|
|
if (params.timeout) {
|
||
|
|
options.timeout = params.timeout as number;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (params.env) {
|
||
|
|
options.env = {
|
||
|
|
...process.env,
|
||
|
|
...(params.env as NodeJS.ProcessEnv),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use execSpawn for security - no shell injection possible
|
||
|
|
const result = await this.smartshell.execSpawn(command, args, options);
|
||
|
|
|
||
|
|
return {
|
||
|
|
success: result.exitCode === 0,
|
||
|
|
result: {
|
||
|
|
command,
|
||
|
|
args,
|
||
|
|
exitCode: result.exitCode,
|
||
|
|
stdout: result.stdout,
|
||
|
|
stderr: result.stderr,
|
||
|
|
signal: result.signal,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'which': {
|
||
|
|
try {
|
||
|
|
const commandPath = await plugins.smartshell.which(params.command as string);
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
result: {
|
||
|
|
command: params.command,
|
||
|
|
path: commandPath,
|
||
|
|
exists: true,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
} catch {
|
||
|
|
return {
|
||
|
|
success: true,
|
||
|
|
result: {
|
||
|
|
command: params.command,
|
||
|
|
path: null,
|
||
|
|
exists: false,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
default:
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: `Unknown action: ${action}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
error: error instanceof Error ? error.message : String(error),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public getCallSummary(action: string, params: Record<string, unknown>): string {
|
||
|
|
switch (action) {
|
||
|
|
case 'execute': {
|
||
|
|
const command = params.command as string;
|
||
|
|
const args = (params.args as string[]) || [];
|
||
|
|
const fullCommand = [command, ...args].join(' ');
|
||
|
|
let summary = `Execute: ${fullCommand}`;
|
||
|
|
|
||
|
|
if (params.cwd) {
|
||
|
|
summary += ` (in ${params.cwd})`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (params.timeout) {
|
||
|
|
summary += ` [timeout: ${params.timeout}ms]`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (params.env && Object.keys(params.env as object).length > 0) {
|
||
|
|
const envKeys = Object.keys(params.env as object).join(', ');
|
||
|
|
summary += ` [env: ${envKeys}]`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return summary;
|
||
|
|
}
|
||
|
|
|
||
|
|
case 'which':
|
||
|
|
return `Check if command "${params.command}" exists and get its path`;
|
||
|
|
|
||
|
|
default:
|
||
|
|
return `Unknown action: ${action}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|