@aigne/agent-library 1.23.0-beta.7 → 1.23.0-beta.8
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.md +7 -0
- package/README.md +3 -0
- package/lib/cjs/bash/index.d.ts +124 -0
- package/lib/cjs/bash/index.js +359 -0
- package/lib/cjs/utils/mutex.d.ts +6 -0
- package/lib/cjs/utils/mutex.js +28 -0
- package/lib/dts/bash/index.d.ts +124 -0
- package/lib/dts/utils/mutex.d.ts +6 -0
- package/lib/esm/bash/index.d.ts +124 -0
- package/lib/esm/bash/index.js +322 -0
- package/lib/esm/utils/mutex.d.ts +6 -0
- package/lib/esm/utils/mutex.js +24 -0
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* @aigne/core bumped to 1.22.0
|
|
8
8
|
* @aigne/openai bumped to 0.3.4
|
|
9
9
|
|
|
10
|
+
## [1.23.0-beta.8](https://github.com/AIGNE-io/aigne-framework/compare/agent-library-v1.23.0-beta.7...agent-library-v1.23.0-beta.8) (2025-12-12)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **agent-library:** add BashAgent with sandbox support ([#816](https://github.com/AIGNE-io/aigne-framework/issues/816)) ([0d4feee](https://github.com/AIGNE-io/aigne-framework/commit/0d4feeeac2b71df1c4d725adeee76c9318ce8e02))
|
|
16
|
+
|
|
10
17
|
## [1.23.0-beta.7](https://github.com/AIGNE-io/aigne-framework/compare/agent-library-v1.23.0-beta.6...agent-library-v1.23.0-beta.7) (2025-12-11)
|
|
11
18
|
|
|
12
19
|
|
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Collection of agent libraries for [AIGNE Framework](https://github.com/AIGNE-io/
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
31
31
|
* **Orchestrator Agent**: Provides OrchestratorAgent implementation for coordinating workflows between multiple agents
|
|
32
|
+
* **Bash Agent**: Secure execution of bash scripts with sandboxed environment and comprehensive output handling
|
|
32
33
|
* **Task Concurrency**: Supports parallel execution of multiple tasks to improve processing efficiency
|
|
33
34
|
* **Planning & Execution**: Automatically generates execution plans and executes them step by step
|
|
34
35
|
* **Result Synthesis**: Intelligently synthesizes results from multiple steps and tasks
|
|
@@ -62,6 +63,8 @@ The library provides the following components:
|
|
|
62
63
|
|
|
63
64
|
* **[Orchestrator Agent](src/orchestrator/README.md)**: A sophisticated agent pattern that enables autonomous task planning and execution through a three-phase architecture: Planner → Worker → Completer. It breaks down complex objectives into manageable tasks, executes them iteratively, and synthesizes the final results. Perfect for coordinating complex workflows and multi-step tasks.
|
|
64
65
|
|
|
66
|
+
* **[Bash Agent](src/bash/README.md)**: Enables secure execution of bash scripts within a sandboxed environment. Provides controlled access to system commands, network resources, and the filesystem while returning comprehensive execution results including stdout, stderr, and exit codes. Ideal for running system commands, interacting with CLI tools, and executing shell scripts safely.
|
|
67
|
+
|
|
65
68
|
## License
|
|
66
69
|
|
|
67
70
|
Elastic-2.0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentResponseStream, type Message } from "@aigne/core";
|
|
3
|
+
import { type NestAgentSchema } from "@aigne/core/loader/agent-yaml.js";
|
|
4
|
+
import { type LoadOptions } from "@aigne/core/loader/index.js";
|
|
5
|
+
import { type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
6
|
+
export interface BashAgentOptions extends AgentOptions<BashAgentInput, BashAgentOutput> {
|
|
7
|
+
sandbox?: Partial<{
|
|
8
|
+
[K in keyof SandboxRuntimeConfig]: Partial<SandboxRuntimeConfig[K]>;
|
|
9
|
+
}> | boolean;
|
|
10
|
+
inputKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional timeout for script execution in milliseconds
|
|
13
|
+
* @default 60000 (60 seconds)
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Optional permissions configuration for command execution control
|
|
18
|
+
* Inspired by Claude Code's permission system
|
|
19
|
+
*/
|
|
20
|
+
permissions?: {
|
|
21
|
+
/**
|
|
22
|
+
* Whitelist: Commands that are allowed to execute without approval
|
|
23
|
+
* Supports exact match or prefix match with ':*' wildcard
|
|
24
|
+
* Examples: ['npm run test:*', 'git status', 'ls:*']
|
|
25
|
+
*/
|
|
26
|
+
allow?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Blacklist: Commands that are completely forbidden
|
|
29
|
+
* Takes highest priority over allow and defaultMode
|
|
30
|
+
* Examples: ['rm:*', 'sudo:*', 'curl:*']
|
|
31
|
+
*/
|
|
32
|
+
deny?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Default permission mode when command doesn't match allow/deny lists
|
|
35
|
+
* @default 'allow'
|
|
36
|
+
*/
|
|
37
|
+
defaultMode?: "allow" | "ask" | "deny";
|
|
38
|
+
/**
|
|
39
|
+
* Callback function invoked when a command requires user approval (ask mode)
|
|
40
|
+
* Return true to approve, false to reject
|
|
41
|
+
* @param script - The script that requires approval
|
|
42
|
+
* @returns Promise resolving to approval decision
|
|
43
|
+
*/
|
|
44
|
+
guard?: BashAgent["guard"];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface LoadBashAgentOptions extends Omit<BashAgentOptions, "permissions"> {
|
|
48
|
+
permissions?: Omit<NonNullable<BashAgentOptions["permissions"]>, "guard"> & {
|
|
49
|
+
guard?: NestAgentSchema;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface BashAgentInput extends Message {
|
|
53
|
+
script?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface BashAgentOutput extends Message {
|
|
56
|
+
stdout?: string;
|
|
57
|
+
stderr?: string;
|
|
58
|
+
exitCode?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class BashAgent extends Agent<BashAgentInput, BashAgentOutput> {
|
|
61
|
+
options: BashAgentOptions;
|
|
62
|
+
static load(options: {
|
|
63
|
+
filepath: string;
|
|
64
|
+
parsed: LoadBashAgentOptions;
|
|
65
|
+
options?: LoadOptions;
|
|
66
|
+
}): Promise<Agent<any, any>>;
|
|
67
|
+
constructor(options: BashAgentOptions);
|
|
68
|
+
inputKey?: string;
|
|
69
|
+
guard?: Agent<{
|
|
70
|
+
script?: string;
|
|
71
|
+
}, {
|
|
72
|
+
approved: boolean;
|
|
73
|
+
reason?: string;
|
|
74
|
+
}>;
|
|
75
|
+
process(input: BashAgentInput, options: AgentInvokeOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
76
|
+
spawn(command: string, args?: string[], options?: SpawnOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
77
|
+
runInSandbox<T>(config: Exclude<BashAgentOptions["sandbox"], boolean>, script: string, task: (script: string) => Promise<T>): Promise<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Check permission for executing a script
|
|
80
|
+
* Permission priority: deny > allow > defaultMode
|
|
81
|
+
*
|
|
82
|
+
* For complex commands (with pipes, chaining, etc.), each sub-command
|
|
83
|
+
* is validated separately. All sub-commands must pass permission checks.
|
|
84
|
+
*
|
|
85
|
+
* @param script - The script to check permission for
|
|
86
|
+
* @returns Permission decision: 'allow', 'ask', or 'deny'
|
|
87
|
+
*/
|
|
88
|
+
checkPermission(script: string): Promise<"allow" | "ask" | "deny">;
|
|
89
|
+
/**
|
|
90
|
+
* Split a script into individual commands by pipes, command chaining, etc.
|
|
91
|
+
* Separators: | (pipe), && (AND), || (OR), & (background), ; (sequential), \n (newline)
|
|
92
|
+
*
|
|
93
|
+
* Note: Redirection operators (>, >>, <, <<, &>, 2>, etc.) are treated as part of
|
|
94
|
+
* the command, not as separators.
|
|
95
|
+
*
|
|
96
|
+
* @param script - The script to split
|
|
97
|
+
* @returns Array of individual commands
|
|
98
|
+
*/
|
|
99
|
+
private splitCommands;
|
|
100
|
+
/**
|
|
101
|
+
* Check permission for a single command
|
|
102
|
+
* @param command - The command to check
|
|
103
|
+
* @param permissions - Permission configuration
|
|
104
|
+
* @returns Permission decision for this command
|
|
105
|
+
*/
|
|
106
|
+
private checkSingleCommandPermission;
|
|
107
|
+
/**
|
|
108
|
+
* Match a single command against a permission pattern
|
|
109
|
+
* Supports exact match and prefix match with ':*' wildcard
|
|
110
|
+
*
|
|
111
|
+
* Note: This method is called for individual commands after splitting,
|
|
112
|
+
* so it doesn't need to handle complex command chaining.
|
|
113
|
+
*
|
|
114
|
+
* Examples:
|
|
115
|
+
* - "ls:*" matches "ls", "ls -la", "ls:option"
|
|
116
|
+
* - "npm run test:*" matches "npm run test", "npm run test:unit", "npm run test arg"
|
|
117
|
+
*
|
|
118
|
+
* @param command - The command to match (should be a single command)
|
|
119
|
+
* @param pattern - The pattern to match against
|
|
120
|
+
* @returns true if command matches pattern
|
|
121
|
+
*/
|
|
122
|
+
private matchPattern;
|
|
123
|
+
}
|
|
124
|
+
export default BashAgent;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BashAgent = void 0;
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const core_1 = require("@aigne/core");
|
|
39
|
+
const agent_yaml_js_1 = require("@aigne/core/loader/agent-yaml.js");
|
|
40
|
+
const index_js_1 = require("@aigne/core/loader/index.js");
|
|
41
|
+
const schema_js_1 = require("@aigne/core/loader/schema.js");
|
|
42
|
+
const sandbox_runtime_1 = require("@anthropic-ai/sandbox-runtime");
|
|
43
|
+
const ripgrep_1 = require("@vscode/ripgrep");
|
|
44
|
+
const zod_1 = __importStar(require("zod"));
|
|
45
|
+
const mutex_js_1 = require("../utils/mutex.js");
|
|
46
|
+
const DEFAULT_TIMEOUT = 60e3; // 60 seconds
|
|
47
|
+
let sandboxInitialization;
|
|
48
|
+
const mutex = new mutex_js_1.Mutex();
|
|
49
|
+
class BashAgent extends core_1.Agent {
|
|
50
|
+
options;
|
|
51
|
+
static async load(options) {
|
|
52
|
+
const schema = getBashAgentSchema({ filepath: options.filepath });
|
|
53
|
+
const parsed = await schema.parseAsync(options.parsed);
|
|
54
|
+
return new BashAgent({
|
|
55
|
+
...parsed,
|
|
56
|
+
permissions: {
|
|
57
|
+
...parsed.permissions,
|
|
58
|
+
guard: parsed.permissions?.guard
|
|
59
|
+
? await (0, index_js_1.loadNestAgent)(options.filepath, parsed.permissions.guard, options.options ?? {}, {
|
|
60
|
+
outputSchema: zod_1.default.object({
|
|
61
|
+
approved: zod_1.default.boolean().describe("Whether the command is approved by the user."),
|
|
62
|
+
reason: zod_1.default.string().describe("Optional reason for rejection.").optional(),
|
|
63
|
+
}),
|
|
64
|
+
})
|
|
65
|
+
: undefined,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
constructor(options) {
|
|
70
|
+
super({
|
|
71
|
+
name: "Bash",
|
|
72
|
+
description: `\
|
|
73
|
+
Execute bash scripts and return stdout and stderr output.
|
|
74
|
+
|
|
75
|
+
When to use:
|
|
76
|
+
- Running system commands or bash scripts
|
|
77
|
+
- Interacting with command-line tools
|
|
78
|
+
`,
|
|
79
|
+
...options,
|
|
80
|
+
inputSchema: zod_1.default.object({
|
|
81
|
+
[options.inputKey || "script"]: zod_1.default.string().describe("The bash script to execute."),
|
|
82
|
+
}),
|
|
83
|
+
outputSchema: zod_1.default.object({
|
|
84
|
+
stdout: zod_1.default.string().describe("The standard output from the bash script.").optional(),
|
|
85
|
+
stderr: zod_1.default.string().describe("The standard error output from the bash script.").optional(),
|
|
86
|
+
exitCode: zod_1.default.number().describe("The exit code of the bash script execution.").optional(),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
this.options = options;
|
|
90
|
+
this.guard = this.options.permissions?.guard;
|
|
91
|
+
this.inputKey = this.options.inputKey;
|
|
92
|
+
}
|
|
93
|
+
inputKey;
|
|
94
|
+
guard;
|
|
95
|
+
async process(input, options) {
|
|
96
|
+
const script = input[this.inputKey || "script"];
|
|
97
|
+
if (typeof script !== "string")
|
|
98
|
+
throw new Error(`Invalid or missing script input: ${this.inputKey || "script"}`);
|
|
99
|
+
// Permission check
|
|
100
|
+
const permission = await this.checkPermission(script);
|
|
101
|
+
if (permission === "deny") {
|
|
102
|
+
throw new Error(`Command blocked by permissions: ${script}`);
|
|
103
|
+
}
|
|
104
|
+
if (permission === "ask") {
|
|
105
|
+
if (!this.guard) {
|
|
106
|
+
throw new Error(`No guard agent configured for permission 'ask'`);
|
|
107
|
+
}
|
|
108
|
+
const { approved, reason } = await this.invokeChildAgent(this.guard, input, {
|
|
109
|
+
...options,
|
|
110
|
+
streaming: false,
|
|
111
|
+
});
|
|
112
|
+
if (!approved) {
|
|
113
|
+
throw new Error(`Command rejected by guard agent (${this.guard.name}): ${script}, reason: ${reason || "no reason provided"}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const platform = {
|
|
117
|
+
win32: "windows",
|
|
118
|
+
darwin: "macos",
|
|
119
|
+
linux: "linux",
|
|
120
|
+
}[globalThis.process.platform] || "unknown";
|
|
121
|
+
if (this.options.sandbox === false) {
|
|
122
|
+
return this.spawn("bash", ["-c", script]);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (!sandbox_runtime_1.SandboxManager.isSupportedPlatform(platform)) {
|
|
126
|
+
throw new Error(`Sandboxed execution is not supported on this platform ${platform}`);
|
|
127
|
+
}
|
|
128
|
+
return await this.runInSandbox(typeof this.options.sandbox === "boolean" ? {} : this.options.sandbox, script, async (sandboxedCommand) => {
|
|
129
|
+
return this.spawn(sandboxedCommand, undefined, {
|
|
130
|
+
shell: true,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async spawn(command, args, options) {
|
|
136
|
+
return new ReadableStream({
|
|
137
|
+
start: (controller) => {
|
|
138
|
+
try {
|
|
139
|
+
const timeout = this.options.timeout ?? DEFAULT_TIMEOUT;
|
|
140
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
141
|
+
...options,
|
|
142
|
+
stdio: "pipe",
|
|
143
|
+
timeout,
|
|
144
|
+
});
|
|
145
|
+
let stderr = "";
|
|
146
|
+
child.stdout.on("data", (chunk) => {
|
|
147
|
+
controller.enqueue({ delta: { text: { stdout: chunk.toString() } } });
|
|
148
|
+
});
|
|
149
|
+
child.stderr.on("data", (chunk) => {
|
|
150
|
+
controller.enqueue({ delta: { text: { stderr: chunk.toString() } } });
|
|
151
|
+
stderr += chunk.toString();
|
|
152
|
+
});
|
|
153
|
+
child.on("error", (error) => {
|
|
154
|
+
controller.error(error);
|
|
155
|
+
});
|
|
156
|
+
child.on("close", (code, signal) => {
|
|
157
|
+
// Handle timeout or killed by signal
|
|
158
|
+
if (signal) {
|
|
159
|
+
const timeoutHint = signal === "SIGTERM" ? ` (likely timeout ${timeout})` : "";
|
|
160
|
+
controller.error(new Error(`Bash script killed by signal ${signal}${timeoutHint}: ${stderr}`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Handle normal exit
|
|
164
|
+
if (typeof code === "number") {
|
|
165
|
+
if (code === 0) {
|
|
166
|
+
controller.enqueue({ delta: { json: { exitCode: code } } });
|
|
167
|
+
controller.close();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
controller.error(new Error(`Bash script exited with code ${code}: ${stderr}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Unexpected case: no code and no signal
|
|
175
|
+
controller.error(new Error(`Bash script closed unexpectedly: ${stderr}`));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
controller.error(error);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async runInSandbox(config, script, task) {
|
|
186
|
+
return await mutex.runExclusive(async () => {
|
|
187
|
+
sandboxInitialization ??= sandbox_runtime_1.SandboxManager.initialize({
|
|
188
|
+
network: {
|
|
189
|
+
allowedDomains: [],
|
|
190
|
+
deniedDomains: [],
|
|
191
|
+
},
|
|
192
|
+
filesystem: {
|
|
193
|
+
denyRead: [],
|
|
194
|
+
denyWrite: [],
|
|
195
|
+
allowWrite: [],
|
|
196
|
+
},
|
|
197
|
+
ripgrep: {
|
|
198
|
+
command: ripgrep_1.rgPath,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
await sandboxInitialization;
|
|
202
|
+
sandbox_runtime_1.SandboxManager.updateConfig({
|
|
203
|
+
...config,
|
|
204
|
+
network: {
|
|
205
|
+
...config?.network,
|
|
206
|
+
allowedDomains: config?.network?.allowedDomains || [],
|
|
207
|
+
deniedDomains: config?.network?.deniedDomains || [],
|
|
208
|
+
},
|
|
209
|
+
filesystem: {
|
|
210
|
+
...config?.filesystem,
|
|
211
|
+
denyRead: config?.filesystem?.denyRead || [],
|
|
212
|
+
denyWrite: config?.filesystem?.denyWrite || [],
|
|
213
|
+
allowWrite: config?.filesystem?.allowWrite || [],
|
|
214
|
+
},
|
|
215
|
+
ripgrep: {
|
|
216
|
+
command: ripgrep_1.rgPath,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const sandboxedCommand = await sandbox_runtime_1.SandboxManager.wrapWithSandbox(script);
|
|
220
|
+
return await task(sandboxedCommand);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Check permission for executing a script
|
|
225
|
+
* Permission priority: deny > allow > defaultMode
|
|
226
|
+
*
|
|
227
|
+
* For complex commands (with pipes, chaining, etc.), each sub-command
|
|
228
|
+
* is validated separately. All sub-commands must pass permission checks.
|
|
229
|
+
*
|
|
230
|
+
* @param script - The script to check permission for
|
|
231
|
+
* @returns Permission decision: 'allow', 'ask', or 'deny'
|
|
232
|
+
*/
|
|
233
|
+
async checkPermission(script) {
|
|
234
|
+
const { permissions } = this.options;
|
|
235
|
+
if (!permissions) {
|
|
236
|
+
return "allow"; // No permissions configured, default to allow
|
|
237
|
+
}
|
|
238
|
+
// Split complex commands into individual commands
|
|
239
|
+
const commands = this.splitCommands(script);
|
|
240
|
+
// Check permission for each command
|
|
241
|
+
for (const command of commands) {
|
|
242
|
+
const commandPermission = this.checkSingleCommandPermission(command, permissions);
|
|
243
|
+
// If any command is denied, deny the whole script
|
|
244
|
+
if (commandPermission === "deny") {
|
|
245
|
+
return "deny";
|
|
246
|
+
}
|
|
247
|
+
// If any command requires asking, the whole script requires asking
|
|
248
|
+
if (commandPermission === "ask") {
|
|
249
|
+
return "ask";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// All commands are allowed
|
|
253
|
+
return "allow";
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Split a script into individual commands by pipes, command chaining, etc.
|
|
257
|
+
* Separators: | (pipe), && (AND), || (OR), & (background), ; (sequential), \n (newline)
|
|
258
|
+
*
|
|
259
|
+
* Note: Redirection operators (>, >>, <, <<, &>, 2>, etc.) are treated as part of
|
|
260
|
+
* the command, not as separators.
|
|
261
|
+
*
|
|
262
|
+
* @param script - The script to split
|
|
263
|
+
* @returns Array of individual commands
|
|
264
|
+
*/
|
|
265
|
+
splitCommands(script) {
|
|
266
|
+
// Split by pipes, &&, ||, &, ;, and newlines
|
|
267
|
+
// IMPORTANT: Match longer patterns first to avoid incorrect splitting:
|
|
268
|
+
// - && and || before single & and |
|
|
269
|
+
// - Must use negative lookbehind/lookahead to avoid matching &> (redirect)
|
|
270
|
+
// Pattern explanation: (?<!&) means "not preceded by &", (?!>) means "not followed by >"
|
|
271
|
+
const parts = script.split(/(\||&&|\|\||(?<!&)&(?!>)|;|\n)/);
|
|
272
|
+
const commands = [];
|
|
273
|
+
for (let i = 0; i < parts.length; i++) {
|
|
274
|
+
const part = parts[i];
|
|
275
|
+
if (!part)
|
|
276
|
+
continue;
|
|
277
|
+
const trimmedPart = part.trim();
|
|
278
|
+
// Skip empty parts and separator tokens
|
|
279
|
+
if (trimmedPart && !["|", "&&", "||", "&", ";"].includes(trimmedPart)) {
|
|
280
|
+
commands.push(trimmedPart);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return commands.length > 0 ? commands : [script];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Check permission for a single command
|
|
287
|
+
* @param command - The command to check
|
|
288
|
+
* @param permissions - Permission configuration
|
|
289
|
+
* @returns Permission decision for this command
|
|
290
|
+
*/
|
|
291
|
+
checkSingleCommandPermission(command, permissions) {
|
|
292
|
+
// Priority 1: Check deny list (highest priority)
|
|
293
|
+
if (permissions.deny?.some((pattern) => this.matchPattern(command, pattern))) {
|
|
294
|
+
return "deny";
|
|
295
|
+
}
|
|
296
|
+
// Priority 2: Check allow list
|
|
297
|
+
if (permissions.allow?.some((pattern) => this.matchPattern(command, pattern))) {
|
|
298
|
+
return "allow";
|
|
299
|
+
}
|
|
300
|
+
// Priority 3: Apply default mode
|
|
301
|
+
return permissions.defaultMode || "allow";
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Match a single command against a permission pattern
|
|
305
|
+
* Supports exact match and prefix match with ':*' wildcard
|
|
306
|
+
*
|
|
307
|
+
* Note: This method is called for individual commands after splitting,
|
|
308
|
+
* so it doesn't need to handle complex command chaining.
|
|
309
|
+
*
|
|
310
|
+
* Examples:
|
|
311
|
+
* - "ls:*" matches "ls", "ls -la", "ls:option"
|
|
312
|
+
* - "npm run test:*" matches "npm run test", "npm run test:unit", "npm run test arg"
|
|
313
|
+
*
|
|
314
|
+
* @param command - The command to match (should be a single command)
|
|
315
|
+
* @param pattern - The pattern to match against
|
|
316
|
+
* @returns true if command matches pattern
|
|
317
|
+
*/
|
|
318
|
+
matchPattern(command, pattern) {
|
|
319
|
+
// Trim whitespace
|
|
320
|
+
command = command.trim();
|
|
321
|
+
pattern = pattern.trim();
|
|
322
|
+
// Exact match
|
|
323
|
+
if (pattern === command) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
// Prefix match with ':*' wildcard
|
|
327
|
+
if (pattern.endsWith(":*")) {
|
|
328
|
+
const prefix = pattern.slice(0, -2).trim();
|
|
329
|
+
// Match if command equals prefix or starts with prefix followed by space or colon
|
|
330
|
+
return command === prefix || command.startsWith(prefix) || command.startsWith(`${prefix}:`);
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
exports.BashAgent = BashAgent;
|
|
336
|
+
exports.default = BashAgent;
|
|
337
|
+
function getBashAgentSchema({ filepath }) {
|
|
338
|
+
const nestAgentSchema = (0, agent_yaml_js_1.getNestAgentSchema)({ filepath });
|
|
339
|
+
return (0, schema_js_1.camelizeSchema)(zod_1.default.object({
|
|
340
|
+
sandbox: (0, schema_js_1.optionalize)(zod_1.default.union([makeShapePropertiesOptions(sandbox_runtime_1.SandboxRuntimeConfigSchema, 2), zod_1.default.boolean()])),
|
|
341
|
+
inputKey: (0, schema_js_1.optionalize)(zod_1.default.string().describe("The input key for the bash script.")),
|
|
342
|
+
timeout: (0, schema_js_1.optionalize)(zod_1.default.number().describe("Timeout for script execution in milliseconds.")),
|
|
343
|
+
permissions: (0, schema_js_1.optionalize)((0, schema_js_1.camelizeSchema)(zod_1.default.object({
|
|
344
|
+
allow: (0, schema_js_1.optionalize)(zod_1.default.array(zod_1.default.string())),
|
|
345
|
+
deny: (0, schema_js_1.optionalize)(zod_1.default.array(zod_1.default.string())),
|
|
346
|
+
defaultMode: (0, schema_js_1.optionalize)(zod_1.default.enum(["allow", "ask", "deny"])),
|
|
347
|
+
guard: (0, schema_js_1.optionalize)(nestAgentSchema),
|
|
348
|
+
}))),
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
function makeShapePropertiesOptions(schema, depth = 1) {
|
|
352
|
+
return zod_1.default.object(Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => {
|
|
353
|
+
const isObject = value instanceof zod_1.ZodObject;
|
|
354
|
+
if (isObject && depth > 1) {
|
|
355
|
+
return [key, (0, schema_js_1.optionalize)(makeShapePropertiesOptions(value, depth - 1))];
|
|
356
|
+
}
|
|
357
|
+
return [key, (0, schema_js_1.optionalize)(value)];
|
|
358
|
+
})));
|
|
359
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Mutex = void 0;
|
|
4
|
+
class Mutex {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._lock = Promise.resolve();
|
|
7
|
+
}
|
|
8
|
+
_lock;
|
|
9
|
+
lock() {
|
|
10
|
+
let unlockNext;
|
|
11
|
+
const willLock = new Promise((resolve) => {
|
|
12
|
+
unlockNext = resolve;
|
|
13
|
+
});
|
|
14
|
+
const willUnlock = this._lock.then(() => unlockNext);
|
|
15
|
+
this._lock = willLock;
|
|
16
|
+
return willUnlock;
|
|
17
|
+
}
|
|
18
|
+
async runExclusive(callback) {
|
|
19
|
+
const unlock = await this.lock();
|
|
20
|
+
try {
|
|
21
|
+
return await callback();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
unlock();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.Mutex = Mutex;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentResponseStream, type Message } from "@aigne/core";
|
|
3
|
+
import { type NestAgentSchema } from "@aigne/core/loader/agent-yaml.js";
|
|
4
|
+
import { type LoadOptions } from "@aigne/core/loader/index.js";
|
|
5
|
+
import { type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
6
|
+
export interface BashAgentOptions extends AgentOptions<BashAgentInput, BashAgentOutput> {
|
|
7
|
+
sandbox?: Partial<{
|
|
8
|
+
[K in keyof SandboxRuntimeConfig]: Partial<SandboxRuntimeConfig[K]>;
|
|
9
|
+
}> | boolean;
|
|
10
|
+
inputKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional timeout for script execution in milliseconds
|
|
13
|
+
* @default 60000 (60 seconds)
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Optional permissions configuration for command execution control
|
|
18
|
+
* Inspired by Claude Code's permission system
|
|
19
|
+
*/
|
|
20
|
+
permissions?: {
|
|
21
|
+
/**
|
|
22
|
+
* Whitelist: Commands that are allowed to execute without approval
|
|
23
|
+
* Supports exact match or prefix match with ':*' wildcard
|
|
24
|
+
* Examples: ['npm run test:*', 'git status', 'ls:*']
|
|
25
|
+
*/
|
|
26
|
+
allow?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Blacklist: Commands that are completely forbidden
|
|
29
|
+
* Takes highest priority over allow and defaultMode
|
|
30
|
+
* Examples: ['rm:*', 'sudo:*', 'curl:*']
|
|
31
|
+
*/
|
|
32
|
+
deny?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Default permission mode when command doesn't match allow/deny lists
|
|
35
|
+
* @default 'allow'
|
|
36
|
+
*/
|
|
37
|
+
defaultMode?: "allow" | "ask" | "deny";
|
|
38
|
+
/**
|
|
39
|
+
* Callback function invoked when a command requires user approval (ask mode)
|
|
40
|
+
* Return true to approve, false to reject
|
|
41
|
+
* @param script - The script that requires approval
|
|
42
|
+
* @returns Promise resolving to approval decision
|
|
43
|
+
*/
|
|
44
|
+
guard?: BashAgent["guard"];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface LoadBashAgentOptions extends Omit<BashAgentOptions, "permissions"> {
|
|
48
|
+
permissions?: Omit<NonNullable<BashAgentOptions["permissions"]>, "guard"> & {
|
|
49
|
+
guard?: NestAgentSchema;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface BashAgentInput extends Message {
|
|
53
|
+
script?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface BashAgentOutput extends Message {
|
|
56
|
+
stdout?: string;
|
|
57
|
+
stderr?: string;
|
|
58
|
+
exitCode?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class BashAgent extends Agent<BashAgentInput, BashAgentOutput> {
|
|
61
|
+
options: BashAgentOptions;
|
|
62
|
+
static load(options: {
|
|
63
|
+
filepath: string;
|
|
64
|
+
parsed: LoadBashAgentOptions;
|
|
65
|
+
options?: LoadOptions;
|
|
66
|
+
}): Promise<Agent<any, any>>;
|
|
67
|
+
constructor(options: BashAgentOptions);
|
|
68
|
+
inputKey?: string;
|
|
69
|
+
guard?: Agent<{
|
|
70
|
+
script?: string;
|
|
71
|
+
}, {
|
|
72
|
+
approved: boolean;
|
|
73
|
+
reason?: string;
|
|
74
|
+
}>;
|
|
75
|
+
process(input: BashAgentInput, options: AgentInvokeOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
76
|
+
spawn(command: string, args?: string[], options?: SpawnOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
77
|
+
runInSandbox<T>(config: Exclude<BashAgentOptions["sandbox"], boolean>, script: string, task: (script: string) => Promise<T>): Promise<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Check permission for executing a script
|
|
80
|
+
* Permission priority: deny > allow > defaultMode
|
|
81
|
+
*
|
|
82
|
+
* For complex commands (with pipes, chaining, etc.), each sub-command
|
|
83
|
+
* is validated separately. All sub-commands must pass permission checks.
|
|
84
|
+
*
|
|
85
|
+
* @param script - The script to check permission for
|
|
86
|
+
* @returns Permission decision: 'allow', 'ask', or 'deny'
|
|
87
|
+
*/
|
|
88
|
+
checkPermission(script: string): Promise<"allow" | "ask" | "deny">;
|
|
89
|
+
/**
|
|
90
|
+
* Split a script into individual commands by pipes, command chaining, etc.
|
|
91
|
+
* Separators: | (pipe), && (AND), || (OR), & (background), ; (sequential), \n (newline)
|
|
92
|
+
*
|
|
93
|
+
* Note: Redirection operators (>, >>, <, <<, &>, 2>, etc.) are treated as part of
|
|
94
|
+
* the command, not as separators.
|
|
95
|
+
*
|
|
96
|
+
* @param script - The script to split
|
|
97
|
+
* @returns Array of individual commands
|
|
98
|
+
*/
|
|
99
|
+
private splitCommands;
|
|
100
|
+
/**
|
|
101
|
+
* Check permission for a single command
|
|
102
|
+
* @param command - The command to check
|
|
103
|
+
* @param permissions - Permission configuration
|
|
104
|
+
* @returns Permission decision for this command
|
|
105
|
+
*/
|
|
106
|
+
private checkSingleCommandPermission;
|
|
107
|
+
/**
|
|
108
|
+
* Match a single command against a permission pattern
|
|
109
|
+
* Supports exact match and prefix match with ':*' wildcard
|
|
110
|
+
*
|
|
111
|
+
* Note: This method is called for individual commands after splitting,
|
|
112
|
+
* so it doesn't need to handle complex command chaining.
|
|
113
|
+
*
|
|
114
|
+
* Examples:
|
|
115
|
+
* - "ls:*" matches "ls", "ls -la", "ls:option"
|
|
116
|
+
* - "npm run test:*" matches "npm run test", "npm run test:unit", "npm run test arg"
|
|
117
|
+
*
|
|
118
|
+
* @param command - The command to match (should be a single command)
|
|
119
|
+
* @param pattern - The pattern to match against
|
|
120
|
+
* @returns true if command matches pattern
|
|
121
|
+
*/
|
|
122
|
+
private matchPattern;
|
|
123
|
+
}
|
|
124
|
+
export default BashAgent;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentResponseStream, type Message } from "@aigne/core";
|
|
3
|
+
import { type NestAgentSchema } from "@aigne/core/loader/agent-yaml.js";
|
|
4
|
+
import { type LoadOptions } from "@aigne/core/loader/index.js";
|
|
5
|
+
import { type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
6
|
+
export interface BashAgentOptions extends AgentOptions<BashAgentInput, BashAgentOutput> {
|
|
7
|
+
sandbox?: Partial<{
|
|
8
|
+
[K in keyof SandboxRuntimeConfig]: Partial<SandboxRuntimeConfig[K]>;
|
|
9
|
+
}> | boolean;
|
|
10
|
+
inputKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Optional timeout for script execution in milliseconds
|
|
13
|
+
* @default 60000 (60 seconds)
|
|
14
|
+
*/
|
|
15
|
+
timeout?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Optional permissions configuration for command execution control
|
|
18
|
+
* Inspired by Claude Code's permission system
|
|
19
|
+
*/
|
|
20
|
+
permissions?: {
|
|
21
|
+
/**
|
|
22
|
+
* Whitelist: Commands that are allowed to execute without approval
|
|
23
|
+
* Supports exact match or prefix match with ':*' wildcard
|
|
24
|
+
* Examples: ['npm run test:*', 'git status', 'ls:*']
|
|
25
|
+
*/
|
|
26
|
+
allow?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Blacklist: Commands that are completely forbidden
|
|
29
|
+
* Takes highest priority over allow and defaultMode
|
|
30
|
+
* Examples: ['rm:*', 'sudo:*', 'curl:*']
|
|
31
|
+
*/
|
|
32
|
+
deny?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* Default permission mode when command doesn't match allow/deny lists
|
|
35
|
+
* @default 'allow'
|
|
36
|
+
*/
|
|
37
|
+
defaultMode?: "allow" | "ask" | "deny";
|
|
38
|
+
/**
|
|
39
|
+
* Callback function invoked when a command requires user approval (ask mode)
|
|
40
|
+
* Return true to approve, false to reject
|
|
41
|
+
* @param script - The script that requires approval
|
|
42
|
+
* @returns Promise resolving to approval decision
|
|
43
|
+
*/
|
|
44
|
+
guard?: BashAgent["guard"];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export interface LoadBashAgentOptions extends Omit<BashAgentOptions, "permissions"> {
|
|
48
|
+
permissions?: Omit<NonNullable<BashAgentOptions["permissions"]>, "guard"> & {
|
|
49
|
+
guard?: NestAgentSchema;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface BashAgentInput extends Message {
|
|
53
|
+
script?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface BashAgentOutput extends Message {
|
|
56
|
+
stdout?: string;
|
|
57
|
+
stderr?: string;
|
|
58
|
+
exitCode?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class BashAgent extends Agent<BashAgentInput, BashAgentOutput> {
|
|
61
|
+
options: BashAgentOptions;
|
|
62
|
+
static load(options: {
|
|
63
|
+
filepath: string;
|
|
64
|
+
parsed: LoadBashAgentOptions;
|
|
65
|
+
options?: LoadOptions;
|
|
66
|
+
}): Promise<Agent<any, any>>;
|
|
67
|
+
constructor(options: BashAgentOptions);
|
|
68
|
+
inputKey?: string;
|
|
69
|
+
guard?: Agent<{
|
|
70
|
+
script?: string;
|
|
71
|
+
}, {
|
|
72
|
+
approved: boolean;
|
|
73
|
+
reason?: string;
|
|
74
|
+
}>;
|
|
75
|
+
process(input: BashAgentInput, options: AgentInvokeOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
76
|
+
spawn(command: string, args?: string[], options?: SpawnOptions): Promise<AgentResponseStream<BashAgentOutput>>;
|
|
77
|
+
runInSandbox<T>(config: Exclude<BashAgentOptions["sandbox"], boolean>, script: string, task: (script: string) => Promise<T>): Promise<T>;
|
|
78
|
+
/**
|
|
79
|
+
* Check permission for executing a script
|
|
80
|
+
* Permission priority: deny > allow > defaultMode
|
|
81
|
+
*
|
|
82
|
+
* For complex commands (with pipes, chaining, etc.), each sub-command
|
|
83
|
+
* is validated separately. All sub-commands must pass permission checks.
|
|
84
|
+
*
|
|
85
|
+
* @param script - The script to check permission for
|
|
86
|
+
* @returns Permission decision: 'allow', 'ask', or 'deny'
|
|
87
|
+
*/
|
|
88
|
+
checkPermission(script: string): Promise<"allow" | "ask" | "deny">;
|
|
89
|
+
/**
|
|
90
|
+
* Split a script into individual commands by pipes, command chaining, etc.
|
|
91
|
+
* Separators: | (pipe), && (AND), || (OR), & (background), ; (sequential), \n (newline)
|
|
92
|
+
*
|
|
93
|
+
* Note: Redirection operators (>, >>, <, <<, &>, 2>, etc.) are treated as part of
|
|
94
|
+
* the command, not as separators.
|
|
95
|
+
*
|
|
96
|
+
* @param script - The script to split
|
|
97
|
+
* @returns Array of individual commands
|
|
98
|
+
*/
|
|
99
|
+
private splitCommands;
|
|
100
|
+
/**
|
|
101
|
+
* Check permission for a single command
|
|
102
|
+
* @param command - The command to check
|
|
103
|
+
* @param permissions - Permission configuration
|
|
104
|
+
* @returns Permission decision for this command
|
|
105
|
+
*/
|
|
106
|
+
private checkSingleCommandPermission;
|
|
107
|
+
/**
|
|
108
|
+
* Match a single command against a permission pattern
|
|
109
|
+
* Supports exact match and prefix match with ':*' wildcard
|
|
110
|
+
*
|
|
111
|
+
* Note: This method is called for individual commands after splitting,
|
|
112
|
+
* so it doesn't need to handle complex command chaining.
|
|
113
|
+
*
|
|
114
|
+
* Examples:
|
|
115
|
+
* - "ls:*" matches "ls", "ls -la", "ls:option"
|
|
116
|
+
* - "npm run test:*" matches "npm run test", "npm run test:unit", "npm run test arg"
|
|
117
|
+
*
|
|
118
|
+
* @param command - The command to match (should be a single command)
|
|
119
|
+
* @param pattern - The pattern to match against
|
|
120
|
+
* @returns true if command matches pattern
|
|
121
|
+
*/
|
|
122
|
+
private matchPattern;
|
|
123
|
+
}
|
|
124
|
+
export default BashAgent;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Agent, } from "@aigne/core";
|
|
3
|
+
import { getNestAgentSchema } from "@aigne/core/loader/agent-yaml.js";
|
|
4
|
+
import { loadNestAgent } from "@aigne/core/loader/index.js";
|
|
5
|
+
import { camelizeSchema, optionalize } from "@aigne/core/loader/schema.js";
|
|
6
|
+
import { SandboxManager, SandboxRuntimeConfigSchema, } from "@anthropic-ai/sandbox-runtime";
|
|
7
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
8
|
+
import z, { ZodObject } from "zod";
|
|
9
|
+
import { Mutex } from "../utils/mutex.js";
|
|
10
|
+
const DEFAULT_TIMEOUT = 60e3; // 60 seconds
|
|
11
|
+
let sandboxInitialization;
|
|
12
|
+
const mutex = new Mutex();
|
|
13
|
+
export class BashAgent extends Agent {
|
|
14
|
+
options;
|
|
15
|
+
static async load(options) {
|
|
16
|
+
const schema = getBashAgentSchema({ filepath: options.filepath });
|
|
17
|
+
const parsed = await schema.parseAsync(options.parsed);
|
|
18
|
+
return new BashAgent({
|
|
19
|
+
...parsed,
|
|
20
|
+
permissions: {
|
|
21
|
+
...parsed.permissions,
|
|
22
|
+
guard: parsed.permissions?.guard
|
|
23
|
+
? await loadNestAgent(options.filepath, parsed.permissions.guard, options.options ?? {}, {
|
|
24
|
+
outputSchema: z.object({
|
|
25
|
+
approved: z.boolean().describe("Whether the command is approved by the user."),
|
|
26
|
+
reason: z.string().describe("Optional reason for rejection.").optional(),
|
|
27
|
+
}),
|
|
28
|
+
})
|
|
29
|
+
: undefined,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super({
|
|
35
|
+
name: "Bash",
|
|
36
|
+
description: `\
|
|
37
|
+
Execute bash scripts and return stdout and stderr output.
|
|
38
|
+
|
|
39
|
+
When to use:
|
|
40
|
+
- Running system commands or bash scripts
|
|
41
|
+
- Interacting with command-line tools
|
|
42
|
+
`,
|
|
43
|
+
...options,
|
|
44
|
+
inputSchema: z.object({
|
|
45
|
+
[options.inputKey || "script"]: z.string().describe("The bash script to execute."),
|
|
46
|
+
}),
|
|
47
|
+
outputSchema: z.object({
|
|
48
|
+
stdout: z.string().describe("The standard output from the bash script.").optional(),
|
|
49
|
+
stderr: z.string().describe("The standard error output from the bash script.").optional(),
|
|
50
|
+
exitCode: z.number().describe("The exit code of the bash script execution.").optional(),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.guard = this.options.permissions?.guard;
|
|
55
|
+
this.inputKey = this.options.inputKey;
|
|
56
|
+
}
|
|
57
|
+
inputKey;
|
|
58
|
+
guard;
|
|
59
|
+
async process(input, options) {
|
|
60
|
+
const script = input[this.inputKey || "script"];
|
|
61
|
+
if (typeof script !== "string")
|
|
62
|
+
throw new Error(`Invalid or missing script input: ${this.inputKey || "script"}`);
|
|
63
|
+
// Permission check
|
|
64
|
+
const permission = await this.checkPermission(script);
|
|
65
|
+
if (permission === "deny") {
|
|
66
|
+
throw new Error(`Command blocked by permissions: ${script}`);
|
|
67
|
+
}
|
|
68
|
+
if (permission === "ask") {
|
|
69
|
+
if (!this.guard) {
|
|
70
|
+
throw new Error(`No guard agent configured for permission 'ask'`);
|
|
71
|
+
}
|
|
72
|
+
const { approved, reason } = await this.invokeChildAgent(this.guard, input, {
|
|
73
|
+
...options,
|
|
74
|
+
streaming: false,
|
|
75
|
+
});
|
|
76
|
+
if (!approved) {
|
|
77
|
+
throw new Error(`Command rejected by guard agent (${this.guard.name}): ${script}, reason: ${reason || "no reason provided"}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const platform = {
|
|
81
|
+
win32: "windows",
|
|
82
|
+
darwin: "macos",
|
|
83
|
+
linux: "linux",
|
|
84
|
+
}[globalThis.process.platform] || "unknown";
|
|
85
|
+
if (this.options.sandbox === false) {
|
|
86
|
+
return this.spawn("bash", ["-c", script]);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (!SandboxManager.isSupportedPlatform(platform)) {
|
|
90
|
+
throw new Error(`Sandboxed execution is not supported on this platform ${platform}`);
|
|
91
|
+
}
|
|
92
|
+
return await this.runInSandbox(typeof this.options.sandbox === "boolean" ? {} : this.options.sandbox, script, async (sandboxedCommand) => {
|
|
93
|
+
return this.spawn(sandboxedCommand, undefined, {
|
|
94
|
+
shell: true,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async spawn(command, args, options) {
|
|
100
|
+
return new ReadableStream({
|
|
101
|
+
start: (controller) => {
|
|
102
|
+
try {
|
|
103
|
+
const timeout = this.options.timeout ?? DEFAULT_TIMEOUT;
|
|
104
|
+
const child = spawn(command, args, {
|
|
105
|
+
...options,
|
|
106
|
+
stdio: "pipe",
|
|
107
|
+
timeout,
|
|
108
|
+
});
|
|
109
|
+
let stderr = "";
|
|
110
|
+
child.stdout.on("data", (chunk) => {
|
|
111
|
+
controller.enqueue({ delta: { text: { stdout: chunk.toString() } } });
|
|
112
|
+
});
|
|
113
|
+
child.stderr.on("data", (chunk) => {
|
|
114
|
+
controller.enqueue({ delta: { text: { stderr: chunk.toString() } } });
|
|
115
|
+
stderr += chunk.toString();
|
|
116
|
+
});
|
|
117
|
+
child.on("error", (error) => {
|
|
118
|
+
controller.error(error);
|
|
119
|
+
});
|
|
120
|
+
child.on("close", (code, signal) => {
|
|
121
|
+
// Handle timeout or killed by signal
|
|
122
|
+
if (signal) {
|
|
123
|
+
const timeoutHint = signal === "SIGTERM" ? ` (likely timeout ${timeout})` : "";
|
|
124
|
+
controller.error(new Error(`Bash script killed by signal ${signal}${timeoutHint}: ${stderr}`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Handle normal exit
|
|
128
|
+
if (typeof code === "number") {
|
|
129
|
+
if (code === 0) {
|
|
130
|
+
controller.enqueue({ delta: { json: { exitCode: code } } });
|
|
131
|
+
controller.close();
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
controller.error(new Error(`Bash script exited with code ${code}: ${stderr}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Unexpected case: no code and no signal
|
|
139
|
+
controller.error(new Error(`Bash script closed unexpectedly: ${stderr}`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
controller.error(error);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async runInSandbox(config, script, task) {
|
|
150
|
+
return await mutex.runExclusive(async () => {
|
|
151
|
+
sandboxInitialization ??= SandboxManager.initialize({
|
|
152
|
+
network: {
|
|
153
|
+
allowedDomains: [],
|
|
154
|
+
deniedDomains: [],
|
|
155
|
+
},
|
|
156
|
+
filesystem: {
|
|
157
|
+
denyRead: [],
|
|
158
|
+
denyWrite: [],
|
|
159
|
+
allowWrite: [],
|
|
160
|
+
},
|
|
161
|
+
ripgrep: {
|
|
162
|
+
command: rgPath,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
await sandboxInitialization;
|
|
166
|
+
SandboxManager.updateConfig({
|
|
167
|
+
...config,
|
|
168
|
+
network: {
|
|
169
|
+
...config?.network,
|
|
170
|
+
allowedDomains: config?.network?.allowedDomains || [],
|
|
171
|
+
deniedDomains: config?.network?.deniedDomains || [],
|
|
172
|
+
},
|
|
173
|
+
filesystem: {
|
|
174
|
+
...config?.filesystem,
|
|
175
|
+
denyRead: config?.filesystem?.denyRead || [],
|
|
176
|
+
denyWrite: config?.filesystem?.denyWrite || [],
|
|
177
|
+
allowWrite: config?.filesystem?.allowWrite || [],
|
|
178
|
+
},
|
|
179
|
+
ripgrep: {
|
|
180
|
+
command: rgPath,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const sandboxedCommand = await SandboxManager.wrapWithSandbox(script);
|
|
184
|
+
return await task(sandboxedCommand);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check permission for executing a script
|
|
189
|
+
* Permission priority: deny > allow > defaultMode
|
|
190
|
+
*
|
|
191
|
+
* For complex commands (with pipes, chaining, etc.), each sub-command
|
|
192
|
+
* is validated separately. All sub-commands must pass permission checks.
|
|
193
|
+
*
|
|
194
|
+
* @param script - The script to check permission for
|
|
195
|
+
* @returns Permission decision: 'allow', 'ask', or 'deny'
|
|
196
|
+
*/
|
|
197
|
+
async checkPermission(script) {
|
|
198
|
+
const { permissions } = this.options;
|
|
199
|
+
if (!permissions) {
|
|
200
|
+
return "allow"; // No permissions configured, default to allow
|
|
201
|
+
}
|
|
202
|
+
// Split complex commands into individual commands
|
|
203
|
+
const commands = this.splitCommands(script);
|
|
204
|
+
// Check permission for each command
|
|
205
|
+
for (const command of commands) {
|
|
206
|
+
const commandPermission = this.checkSingleCommandPermission(command, permissions);
|
|
207
|
+
// If any command is denied, deny the whole script
|
|
208
|
+
if (commandPermission === "deny") {
|
|
209
|
+
return "deny";
|
|
210
|
+
}
|
|
211
|
+
// If any command requires asking, the whole script requires asking
|
|
212
|
+
if (commandPermission === "ask") {
|
|
213
|
+
return "ask";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// All commands are allowed
|
|
217
|
+
return "allow";
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Split a script into individual commands by pipes, command chaining, etc.
|
|
221
|
+
* Separators: | (pipe), && (AND), || (OR), & (background), ; (sequential), \n (newline)
|
|
222
|
+
*
|
|
223
|
+
* Note: Redirection operators (>, >>, <, <<, &>, 2>, etc.) are treated as part of
|
|
224
|
+
* the command, not as separators.
|
|
225
|
+
*
|
|
226
|
+
* @param script - The script to split
|
|
227
|
+
* @returns Array of individual commands
|
|
228
|
+
*/
|
|
229
|
+
splitCommands(script) {
|
|
230
|
+
// Split by pipes, &&, ||, &, ;, and newlines
|
|
231
|
+
// IMPORTANT: Match longer patterns first to avoid incorrect splitting:
|
|
232
|
+
// - && and || before single & and |
|
|
233
|
+
// - Must use negative lookbehind/lookahead to avoid matching &> (redirect)
|
|
234
|
+
// Pattern explanation: (?<!&) means "not preceded by &", (?!>) means "not followed by >"
|
|
235
|
+
const parts = script.split(/(\||&&|\|\||(?<!&)&(?!>)|;|\n)/);
|
|
236
|
+
const commands = [];
|
|
237
|
+
for (let i = 0; i < parts.length; i++) {
|
|
238
|
+
const part = parts[i];
|
|
239
|
+
if (!part)
|
|
240
|
+
continue;
|
|
241
|
+
const trimmedPart = part.trim();
|
|
242
|
+
// Skip empty parts and separator tokens
|
|
243
|
+
if (trimmedPart && !["|", "&&", "||", "&", ";"].includes(trimmedPart)) {
|
|
244
|
+
commands.push(trimmedPart);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return commands.length > 0 ? commands : [script];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Check permission for a single command
|
|
251
|
+
* @param command - The command to check
|
|
252
|
+
* @param permissions - Permission configuration
|
|
253
|
+
* @returns Permission decision for this command
|
|
254
|
+
*/
|
|
255
|
+
checkSingleCommandPermission(command, permissions) {
|
|
256
|
+
// Priority 1: Check deny list (highest priority)
|
|
257
|
+
if (permissions.deny?.some((pattern) => this.matchPattern(command, pattern))) {
|
|
258
|
+
return "deny";
|
|
259
|
+
}
|
|
260
|
+
// Priority 2: Check allow list
|
|
261
|
+
if (permissions.allow?.some((pattern) => this.matchPattern(command, pattern))) {
|
|
262
|
+
return "allow";
|
|
263
|
+
}
|
|
264
|
+
// Priority 3: Apply default mode
|
|
265
|
+
return permissions.defaultMode || "allow";
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Match a single command against a permission pattern
|
|
269
|
+
* Supports exact match and prefix match with ':*' wildcard
|
|
270
|
+
*
|
|
271
|
+
* Note: This method is called for individual commands after splitting,
|
|
272
|
+
* so it doesn't need to handle complex command chaining.
|
|
273
|
+
*
|
|
274
|
+
* Examples:
|
|
275
|
+
* - "ls:*" matches "ls", "ls -la", "ls:option"
|
|
276
|
+
* - "npm run test:*" matches "npm run test", "npm run test:unit", "npm run test arg"
|
|
277
|
+
*
|
|
278
|
+
* @param command - The command to match (should be a single command)
|
|
279
|
+
* @param pattern - The pattern to match against
|
|
280
|
+
* @returns true if command matches pattern
|
|
281
|
+
*/
|
|
282
|
+
matchPattern(command, pattern) {
|
|
283
|
+
// Trim whitespace
|
|
284
|
+
command = command.trim();
|
|
285
|
+
pattern = pattern.trim();
|
|
286
|
+
// Exact match
|
|
287
|
+
if (pattern === command) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
// Prefix match with ':*' wildcard
|
|
291
|
+
if (pattern.endsWith(":*")) {
|
|
292
|
+
const prefix = pattern.slice(0, -2).trim();
|
|
293
|
+
// Match if command equals prefix or starts with prefix followed by space or colon
|
|
294
|
+
return command === prefix || command.startsWith(prefix) || command.startsWith(`${prefix}:`);
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export default BashAgent;
|
|
300
|
+
function getBashAgentSchema({ filepath }) {
|
|
301
|
+
const nestAgentSchema = getNestAgentSchema({ filepath });
|
|
302
|
+
return camelizeSchema(z.object({
|
|
303
|
+
sandbox: optionalize(z.union([makeShapePropertiesOptions(SandboxRuntimeConfigSchema, 2), z.boolean()])),
|
|
304
|
+
inputKey: optionalize(z.string().describe("The input key for the bash script.")),
|
|
305
|
+
timeout: optionalize(z.number().describe("Timeout for script execution in milliseconds.")),
|
|
306
|
+
permissions: optionalize(camelizeSchema(z.object({
|
|
307
|
+
allow: optionalize(z.array(z.string())),
|
|
308
|
+
deny: optionalize(z.array(z.string())),
|
|
309
|
+
defaultMode: optionalize(z.enum(["allow", "ask", "deny"])),
|
|
310
|
+
guard: optionalize(nestAgentSchema),
|
|
311
|
+
}))),
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
function makeShapePropertiesOptions(schema, depth = 1) {
|
|
315
|
+
return z.object(Object.fromEntries(Object.entries(schema.shape).map(([key, value]) => {
|
|
316
|
+
const isObject = value instanceof ZodObject;
|
|
317
|
+
if (isObject && depth > 1) {
|
|
318
|
+
return [key, optionalize(makeShapePropertiesOptions(value, depth - 1))];
|
|
319
|
+
}
|
|
320
|
+
return [key, optionalize(value)];
|
|
321
|
+
})));
|
|
322
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class Mutex {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._lock = Promise.resolve();
|
|
4
|
+
}
|
|
5
|
+
_lock;
|
|
6
|
+
lock() {
|
|
7
|
+
let unlockNext;
|
|
8
|
+
const willLock = new Promise((resolve) => {
|
|
9
|
+
unlockNext = resolve;
|
|
10
|
+
});
|
|
11
|
+
const willUnlock = this._lock.then(() => unlockNext);
|
|
12
|
+
this._lock = willLock;
|
|
13
|
+
return willUnlock;
|
|
14
|
+
}
|
|
15
|
+
async runExclusive(callback) {
|
|
16
|
+
const unlock = await this.lock();
|
|
17
|
+
try {
|
|
18
|
+
return await callback();
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
unlock();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/agent-library",
|
|
3
|
-
"version": "1.23.0-beta.
|
|
3
|
+
"version": "1.23.0-beta.8",
|
|
4
4
|
"description": "Collection of agent libraries for AIGNE framework",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@aigne/uuid": "^13.0.1",
|
|
51
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.19",
|
|
52
|
+
"@vscode/ripgrep": "^1.15.14",
|
|
51
53
|
"drizzle-orm": "^0.44.5",
|
|
52
54
|
"fastq": "^1.19.1",
|
|
53
55
|
"jsonata": "^2.1.0",
|
|
@@ -56,8 +58,8 @@
|
|
|
56
58
|
"zod": "^3.25.67",
|
|
57
59
|
"zod-to-json-schema": "^3.24.6",
|
|
58
60
|
"@aigne/core": "^1.71.0-beta.6",
|
|
59
|
-
"@aigne/
|
|
60
|
-
"@aigne/
|
|
61
|
+
"@aigne/sqlite": "^0.4.8-beta",
|
|
62
|
+
"@aigne/openai": "^0.16.15-beta.6"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
63
65
|
"@types/bun": "^1.2.22",
|