@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 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,6 @@
1
+ export declare class Mutex {
2
+ constructor();
3
+ private _lock;
4
+ lock(): Promise<() => void>;
5
+ runExclusive<T>(callback: () => Promise<T> | T): Promise<T>;
6
+ }
@@ -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,6 @@
1
+ export declare class Mutex {
2
+ constructor();
3
+ private _lock;
4
+ lock(): Promise<() => void>;
5
+ runExclusive<T>(callback: () => Promise<T> | T): Promise<T>;
6
+ }
@@ -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,6 @@
1
+ export declare class Mutex {
2
+ constructor();
3
+ private _lock;
4
+ lock(): Promise<() => void>;
5
+ runExclusive<T>(callback: () => Promise<T> | T): Promise<T>;
6
+ }
@@ -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.7",
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/openai": "^0.16.15-beta.6",
60
- "@aigne/sqlite": "^0.4.8-beta"
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",