@robota-sdk/agent-tools 3.0.0-beta.6 → 3.0.0-beta.61
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/README.md +93 -19
- package/dist/node/index.cjs +725 -309
- package/dist/node/index.d.cts +173 -39
- package/dist/node/index.d.ts +173 -39
- package/dist/node/index.js +716 -308
- package/package.json +15 -3
package/dist/node/index.js
CHANGED
|
@@ -1,3 +1,272 @@
|
|
|
1
|
+
// src/sandbox/e2b-sandbox-client.ts
|
|
2
|
+
var E2BSandboxClient = class {
|
|
3
|
+
sandbox;
|
|
4
|
+
connectSandbox;
|
|
5
|
+
createSandboxFromSnapshot;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.sandbox = options.sandbox;
|
|
8
|
+
this.connectSandbox = options.connectSandbox;
|
|
9
|
+
this.createSandboxFromSnapshot = options.createSandboxFromSnapshot;
|
|
10
|
+
}
|
|
11
|
+
async run(command, options) {
|
|
12
|
+
const result = await this.sandbox.commands.run(command, {
|
|
13
|
+
background: false,
|
|
14
|
+
timeoutMs: options?.timeoutMs,
|
|
15
|
+
cwd: options?.workingDirectory
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
stdout: result.stdout ?? "",
|
|
19
|
+
stderr: result.stderr ?? "",
|
|
20
|
+
exitCode: result.exitCode ?? result.exit_code ?? 0
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async readFile(path) {
|
|
24
|
+
const content = await this.sandbox.files.read(path);
|
|
25
|
+
return typeof content === "string" ? content : Buffer.from(content).toString("utf8");
|
|
26
|
+
}
|
|
27
|
+
async writeFile(path, content) {
|
|
28
|
+
await this.sandbox.files.write(path, content);
|
|
29
|
+
}
|
|
30
|
+
async snapshot() {
|
|
31
|
+
if (this.sandbox.createSnapshot) {
|
|
32
|
+
const snapshot = await this.sandbox.createSnapshot();
|
|
33
|
+
const snapshotId = snapshot.snapshotId ?? snapshot.id;
|
|
34
|
+
if (!snapshotId) {
|
|
35
|
+
throw new Error("E2B createSnapshot() did not return a snapshot id.");
|
|
36
|
+
}
|
|
37
|
+
return snapshotId;
|
|
38
|
+
}
|
|
39
|
+
const sandboxId = this.sandbox.sandboxId;
|
|
40
|
+
if (!sandboxId) {
|
|
41
|
+
throw new Error("E2B sandboxId is required to create a resumable sandbox snapshot.");
|
|
42
|
+
}
|
|
43
|
+
if (!this.sandbox.pause) {
|
|
44
|
+
throw new Error("E2B sandbox adapter does not expose pause().");
|
|
45
|
+
}
|
|
46
|
+
await this.sandbox.pause();
|
|
47
|
+
return sandboxId;
|
|
48
|
+
}
|
|
49
|
+
async restore(snapshotId) {
|
|
50
|
+
if (this.createSandboxFromSnapshot) {
|
|
51
|
+
this.sandbox = await this.createSandboxFromSnapshot(snapshotId);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (this.connectSandbox) {
|
|
55
|
+
this.sandbox = await this.connectSandbox(snapshotId);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (this.sandbox.sandboxId === snapshotId && this.sandbox.connect) {
|
|
59
|
+
this.sandbox = await this.sandbox.connect();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(
|
|
63
|
+
"E2B sandbox restore requires connectSandbox(snapshotId) or sandbox.connect()."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/sandbox/in-memory-sandbox-client.ts
|
|
69
|
+
var InMemorySandboxClient = class {
|
|
70
|
+
files = /* @__PURE__ */ new Map();
|
|
71
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
72
|
+
runHandler;
|
|
73
|
+
snapshotSequence = 0;
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
for (const [path, content] of Object.entries(options.files ?? {})) {
|
|
76
|
+
this.files.set(path, content);
|
|
77
|
+
}
|
|
78
|
+
this.runHandler = options.runHandler;
|
|
79
|
+
}
|
|
80
|
+
async run(command, options) {
|
|
81
|
+
if (this.runHandler) {
|
|
82
|
+
return this.runHandler(command, options, this.files);
|
|
83
|
+
}
|
|
84
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
85
|
+
}
|
|
86
|
+
async readFile(path) {
|
|
87
|
+
const content = this.files.get(path);
|
|
88
|
+
if (content === void 0) {
|
|
89
|
+
throw new Error(`Sandbox file not found: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
93
|
+
async writeFile(path, content) {
|
|
94
|
+
this.files.set(path, content);
|
|
95
|
+
}
|
|
96
|
+
async snapshot() {
|
|
97
|
+
const snapshotId = `snapshot-${++this.snapshotSequence}`;
|
|
98
|
+
this.snapshots.set(snapshotId, new Map(this.files));
|
|
99
|
+
return snapshotId;
|
|
100
|
+
}
|
|
101
|
+
async restore(snapshotId) {
|
|
102
|
+
const snapshot = this.snapshots.get(snapshotId);
|
|
103
|
+
if (!snapshot) {
|
|
104
|
+
throw new Error(`Sandbox snapshot not found: ${snapshotId}`);
|
|
105
|
+
}
|
|
106
|
+
this.files.clear();
|
|
107
|
+
for (const [path, content] of snapshot.entries()) {
|
|
108
|
+
this.files.set(path, content);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getFile(path) {
|
|
112
|
+
return this.files.get(path);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/sandbox/workspace-manifest.ts
|
|
117
|
+
import { readdir, readFile } from "fs/promises";
|
|
118
|
+
import { isAbsolute, join, posix, resolve } from "path";
|
|
119
|
+
var DEFAULT_TARGET_ROOT = "/workspace";
|
|
120
|
+
var WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
|
|
121
|
+
var SHELL_QUOTE_PATTERN = /'/g;
|
|
122
|
+
async function applyWorkspaceManifest(sandboxClient, manifest, options = {}) {
|
|
123
|
+
if (sandboxClient.applyManifest) {
|
|
124
|
+
return sandboxClient.applyManifest(manifest, options);
|
|
125
|
+
}
|
|
126
|
+
const targetRoot = normalizeSandboxRoot(options.targetRoot ?? DEFAULT_TARGET_ROOT);
|
|
127
|
+
const appliedEntries = [];
|
|
128
|
+
for (const [rawPath, entry] of Object.entries(manifest.entries)) {
|
|
129
|
+
const path = validateWorkspaceManifestPath(rawPath);
|
|
130
|
+
const targetPath = joinSandboxPath(targetRoot, path);
|
|
131
|
+
appliedEntries.push(
|
|
132
|
+
await applyManifestEntry(sandboxClient, path, targetPath, targetRoot, entry, options)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { entries: appliedEntries };
|
|
136
|
+
}
|
|
137
|
+
function validateWorkspaceManifestPath(path) {
|
|
138
|
+
if (path.length === 0) {
|
|
139
|
+
throw new Error("workspace manifest path must not be empty");
|
|
140
|
+
}
|
|
141
|
+
if (path.includes("\0")) {
|
|
142
|
+
throw new Error("workspace manifest path must not contain NUL bytes");
|
|
143
|
+
}
|
|
144
|
+
if (path.startsWith("/") || path.startsWith("\\") || WINDOWS_ABSOLUTE_PATH_PATTERN.test(path)) {
|
|
145
|
+
throw new Error("workspace manifest path must be workspace-relative");
|
|
146
|
+
}
|
|
147
|
+
const parts = path.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
148
|
+
if (parts.length === 0) {
|
|
149
|
+
throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
150
|
+
}
|
|
151
|
+
if (parts.some((part) => part === "..")) {
|
|
152
|
+
throw new Error("workspace manifest path cannot contain traversal segments");
|
|
153
|
+
}
|
|
154
|
+
const normalizedParts = parts.filter((part) => part !== ".");
|
|
155
|
+
if (normalizedParts.length === 0) {
|
|
156
|
+
throw new Error("workspace manifest path must not resolve to the workspace root");
|
|
157
|
+
}
|
|
158
|
+
return normalizedParts.join("/");
|
|
159
|
+
}
|
|
160
|
+
async function applyManifestEntry(sandboxClient, path, targetPath, targetRoot, entry, options) {
|
|
161
|
+
switch (entry.type) {
|
|
162
|
+
case "file":
|
|
163
|
+
await writeSandboxFile(sandboxClient, targetPath, targetRoot, entry.content);
|
|
164
|
+
return createAppliedEntry(path, entry.type);
|
|
165
|
+
case "dir":
|
|
166
|
+
await createSandboxDirectory(sandboxClient, targetPath);
|
|
167
|
+
return createAppliedEntry(path, entry.type);
|
|
168
|
+
case "localFile":
|
|
169
|
+
await copyLocalFile(sandboxClient, entry.src, targetPath, targetRoot, options);
|
|
170
|
+
return createAppliedEntry(path, entry.type);
|
|
171
|
+
case "localDir":
|
|
172
|
+
await copyLocalDirectory(sandboxClient, entry.src, targetPath, options);
|
|
173
|
+
return createAppliedEntry(path, entry.type);
|
|
174
|
+
case "gitRepo":
|
|
175
|
+
await cloneGitRepository(sandboxClient, entry, targetPath);
|
|
176
|
+
return createAppliedEntry(path, entry.type);
|
|
177
|
+
case "s3Mount":
|
|
178
|
+
case "gcsMount":
|
|
179
|
+
case "r2Mount":
|
|
180
|
+
case "azureBlobMount":
|
|
181
|
+
return {
|
|
182
|
+
path,
|
|
183
|
+
type: entry.type,
|
|
184
|
+
status: "unsupported",
|
|
185
|
+
message: `${entry.type} requires a provider-specific sandbox adapter.`
|
|
186
|
+
};
|
|
187
|
+
default:
|
|
188
|
+
return assertUnreachable(entry);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function createAppliedEntry(path, type) {
|
|
192
|
+
return { path, type, status: "applied" };
|
|
193
|
+
}
|
|
194
|
+
async function copyLocalFile(sandboxClient, source, targetPath, targetRoot, options) {
|
|
195
|
+
const hostSourcePath = resolveHostSourcePath(source, options.hostRoot);
|
|
196
|
+
const content = await readFile(hostSourcePath, "utf8");
|
|
197
|
+
await writeSandboxFile(sandboxClient, targetPath, targetRoot, content);
|
|
198
|
+
}
|
|
199
|
+
async function copyLocalDirectory(sandboxClient, source, targetPath, options) {
|
|
200
|
+
const hostSourcePath = resolveHostSourcePath(source, options.hostRoot);
|
|
201
|
+
await copyLocalDirectoryRecursive(sandboxClient, hostSourcePath, targetPath);
|
|
202
|
+
}
|
|
203
|
+
async function copyLocalDirectoryRecursive(sandboxClient, sourcePath, targetPath) {
|
|
204
|
+
await createSandboxDirectory(sandboxClient, targetPath);
|
|
205
|
+
const entries = await readdir(sourcePath, { withFileTypes: true });
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
const childSourcePath = join(sourcePath, entry.name);
|
|
208
|
+
const childTargetPath = joinSandboxPath(targetPath, entry.name);
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
await copyLocalDirectoryRecursive(sandboxClient, childSourcePath, childTargetPath);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (entry.isFile()) {
|
|
214
|
+
const content = await readFile(childSourcePath, "utf8");
|
|
215
|
+
await sandboxClient.writeFile(childTargetPath, content);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function cloneGitRepository(sandboxClient, entry, targetPath) {
|
|
220
|
+
const shallowArgs = entry.shallow === false ? "" : " --depth 1";
|
|
221
|
+
const refArgs = entry.ref ? ` --branch ${quoteShellArg(entry.ref)}` : "";
|
|
222
|
+
await runSandboxCommand(
|
|
223
|
+
sandboxClient,
|
|
224
|
+
`git clone${shallowArgs}${refArgs} ${quoteShellArg(entry.url)} ${quoteShellArg(targetPath)}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
async function writeSandboxFile(sandboxClient, targetPath, targetRoot, content) {
|
|
228
|
+
const parentPath = posix.dirname(targetPath);
|
|
229
|
+
if (parentPath !== targetRoot) {
|
|
230
|
+
await createSandboxDirectory(sandboxClient, parentPath);
|
|
231
|
+
}
|
|
232
|
+
await sandboxClient.writeFile(targetPath, content);
|
|
233
|
+
}
|
|
234
|
+
async function createSandboxDirectory(sandboxClient, targetPath) {
|
|
235
|
+
await runSandboxCommand(sandboxClient, `mkdir -p ${quoteShellArg(targetPath)}`);
|
|
236
|
+
}
|
|
237
|
+
async function runSandboxCommand(sandboxClient, command) {
|
|
238
|
+
const result = await sandboxClient.run(command);
|
|
239
|
+
if (result.exitCode !== 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`workspace manifest command failed: ${command}
|
|
242
|
+
${result.stderr ?? result.stdout}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function resolveHostSourcePath(source, hostRoot) {
|
|
247
|
+
return isAbsolute(source) ? resolve(source) : resolve(hostRoot ?? process.cwd(), source);
|
|
248
|
+
}
|
|
249
|
+
function normalizeSandboxRoot(root) {
|
|
250
|
+
const normalized = root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
251
|
+
if (!normalized.startsWith("/")) {
|
|
252
|
+
throw new Error("workspace manifest targetRoot must be an absolute sandbox path");
|
|
253
|
+
}
|
|
254
|
+
return normalized.length === 0 ? "/" : normalized;
|
|
255
|
+
}
|
|
256
|
+
function joinSandboxPath(root, path) {
|
|
257
|
+
const normalizedRoot = normalizeSandboxRoot(root);
|
|
258
|
+
if (normalizedRoot === "/") {
|
|
259
|
+
return `/${path}`;
|
|
260
|
+
}
|
|
261
|
+
return `${normalizedRoot}/${path}`;
|
|
262
|
+
}
|
|
263
|
+
function quoteShellArg(value) {
|
|
264
|
+
return `'${value.replace(SHELL_QUOTE_PATTERN, "'\\''")}'`;
|
|
265
|
+
}
|
|
266
|
+
function assertUnreachable(value) {
|
|
267
|
+
throw new Error(`unsupported workspace manifest entry: ${JSON.stringify(value)}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
1
270
|
// src/registry/tool-registry.ts
|
|
2
271
|
import { ValidationError } from "@robota-sdk/agent-core";
|
|
3
272
|
import { logger } from "@robota-sdk/agent-core";
|
|
@@ -165,7 +434,9 @@ function zodToJsonSchema(schema, options = {}) {
|
|
|
165
434
|
type: "object",
|
|
166
435
|
properties,
|
|
167
436
|
required,
|
|
168
|
-
...options.allowAdditionalProperties
|
|
437
|
+
...(options.allowAdditionalProperties || schemaDef.unknownKeys === "passthrough") && {
|
|
438
|
+
additionalProperties: true
|
|
439
|
+
}
|
|
169
440
|
};
|
|
170
441
|
}
|
|
171
442
|
function convertZodTypeToProperty(typeObj) {
|
|
@@ -244,6 +515,100 @@ function isRequiredField(typeObj) {
|
|
|
244
515
|
return typeDef.typeName !== "ZodOptional" && typeDef.typeName !== "ZodNullable" && typeDef.typeName !== "ZodDefault";
|
|
245
516
|
}
|
|
246
517
|
|
|
518
|
+
// src/implementations/function-tool/parameter-validator.ts
|
|
519
|
+
function validateParameterType(key, value, schema) {
|
|
520
|
+
const expectedType = schema["type"];
|
|
521
|
+
switch (expectedType) {
|
|
522
|
+
case "string":
|
|
523
|
+
if (typeof value !== "string") {
|
|
524
|
+
return `Parameter "${key}" must be a string, got ${typeof value}`;
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
case "number":
|
|
528
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
529
|
+
return `Parameter "${key}" must be a number, got ${typeof value}`;
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
case "boolean":
|
|
533
|
+
if (typeof value !== "boolean") {
|
|
534
|
+
return `Parameter "${key}" must be a boolean, got ${typeof value}`;
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
case "array":
|
|
538
|
+
if (!Array.isArray(value)) {
|
|
539
|
+
return `Parameter "${key}" must be an array, got ${typeof value}`;
|
|
540
|
+
}
|
|
541
|
+
if (schema.items) {
|
|
542
|
+
for (let i = 0; i < value.length; i++) {
|
|
543
|
+
const itemError = validateParameterType(`${key}[${i}]`, value[i], schema.items);
|
|
544
|
+
if (itemError) {
|
|
545
|
+
return itemError;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
break;
|
|
550
|
+
case "object":
|
|
551
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
552
|
+
return `Parameter "${key}" must be an object, got ${typeof value}`;
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
557
|
+
const enumValues = schema.enum;
|
|
558
|
+
let isValidEnum = false;
|
|
559
|
+
for (const enumValue of enumValues) {
|
|
560
|
+
if (value === enumValue) {
|
|
561
|
+
isValidEnum = true;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (!isValidEnum) {
|
|
566
|
+
return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
571
|
+
function getValidationErrors(parameters, schemaRequired, schemaProperties, additionalProperties) {
|
|
572
|
+
const errors = [];
|
|
573
|
+
for (const field of schemaRequired) {
|
|
574
|
+
if (!(field in parameters)) {
|
|
575
|
+
errors.push(`Missing required parameter: ${field}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
579
|
+
const paramSchema = schemaProperties[key];
|
|
580
|
+
if (!paramSchema) {
|
|
581
|
+
if (additionalProperties === true) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (additionalProperties && typeof additionalProperties === "object") {
|
|
585
|
+
const additionalTypeError = validateParameterType(key, value, additionalProperties);
|
|
586
|
+
if (additionalTypeError) errors.push(additionalTypeError);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
errors.push(`Unknown parameter: ${key}`);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const typeError = validateParameterType(key, value, paramSchema);
|
|
593
|
+
if (typeError) {
|
|
594
|
+
errors.push(typeError);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return errors;
|
|
598
|
+
}
|
|
599
|
+
function validateToolParameters(parameters, schemaRequired, schemaProperties, additionalProperties) {
|
|
600
|
+
const errors = getValidationErrors(
|
|
601
|
+
parameters,
|
|
602
|
+
schemaRequired,
|
|
603
|
+
schemaProperties,
|
|
604
|
+
additionalProperties
|
|
605
|
+
);
|
|
606
|
+
return {
|
|
607
|
+
isValid: errors.length === 0,
|
|
608
|
+
errors
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
247
612
|
// src/implementations/function-tool.ts
|
|
248
613
|
var FunctionTool = class {
|
|
249
614
|
schema;
|
|
@@ -274,7 +639,12 @@ var FunctionTool = class {
|
|
|
274
639
|
async execute(parameters, context) {
|
|
275
640
|
const toolName = this.schema.name;
|
|
276
641
|
if (!this.validate(parameters)) {
|
|
277
|
-
const errors =
|
|
642
|
+
const errors = getValidationErrors(
|
|
643
|
+
parameters,
|
|
644
|
+
this.schema.parameters.required || [],
|
|
645
|
+
this.schema.parameters.properties || {},
|
|
646
|
+
this.schema.parameters.additionalProperties
|
|
647
|
+
);
|
|
278
648
|
throw new ValidationError2(`Invalid parameters for tool "${toolName}": ${errors.join(", ")}`);
|
|
279
649
|
}
|
|
280
650
|
const startTime = Date.now();
|
|
@@ -310,17 +680,23 @@ var FunctionTool = class {
|
|
|
310
680
|
* Validate parameters (simple boolean result)
|
|
311
681
|
*/
|
|
312
682
|
validate(parameters) {
|
|
313
|
-
return
|
|
683
|
+
return getValidationErrors(
|
|
684
|
+
parameters,
|
|
685
|
+
this.schema.parameters.required || [],
|
|
686
|
+
this.schema.parameters.properties || {},
|
|
687
|
+
this.schema.parameters.additionalProperties
|
|
688
|
+
).length === 0;
|
|
314
689
|
}
|
|
315
690
|
/**
|
|
316
691
|
* Validate tool parameters with detailed result
|
|
317
692
|
*/
|
|
318
693
|
validateParameters(parameters) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
694
|
+
return validateToolParameters(
|
|
695
|
+
parameters,
|
|
696
|
+
this.schema.parameters.required || [],
|
|
697
|
+
this.schema.parameters.properties || {},
|
|
698
|
+
this.schema.parameters.additionalProperties
|
|
699
|
+
);
|
|
324
700
|
}
|
|
325
701
|
/**
|
|
326
702
|
* Get tool description
|
|
@@ -328,86 +704,6 @@ var FunctionTool = class {
|
|
|
328
704
|
getDescription() {
|
|
329
705
|
return this.schema.description;
|
|
330
706
|
}
|
|
331
|
-
/**
|
|
332
|
-
* Get detailed validation errors
|
|
333
|
-
*/
|
|
334
|
-
getValidationErrors(parameters) {
|
|
335
|
-
const errors = [];
|
|
336
|
-
const required = this.schema.parameters.required || [];
|
|
337
|
-
const properties = this.schema.parameters.properties || {};
|
|
338
|
-
for (const field of required) {
|
|
339
|
-
if (!(field in parameters)) {
|
|
340
|
-
errors.push(`Missing required parameter: ${field}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
344
|
-
const paramSchema = properties[key];
|
|
345
|
-
if (!paramSchema) {
|
|
346
|
-
errors.push(`Unknown parameter: ${key}`);
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const typeError = this.validateParameterType(key, value, paramSchema);
|
|
350
|
-
if (typeError) {
|
|
351
|
-
errors.push(typeError);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return errors;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Validate individual parameter type
|
|
358
|
-
*/
|
|
359
|
-
validateParameterType(key, value, schema) {
|
|
360
|
-
const expectedType = schema["type"];
|
|
361
|
-
switch (expectedType) {
|
|
362
|
-
case "string":
|
|
363
|
-
if (typeof value !== "string") {
|
|
364
|
-
return `Parameter "${key}" must be a string, got ${typeof value}`;
|
|
365
|
-
}
|
|
366
|
-
break;
|
|
367
|
-
case "number":
|
|
368
|
-
if (typeof value !== "number" || isNaN(value)) {
|
|
369
|
-
return `Parameter "${key}" must be a number, got ${typeof value}`;
|
|
370
|
-
}
|
|
371
|
-
break;
|
|
372
|
-
case "boolean":
|
|
373
|
-
if (typeof value !== "boolean") {
|
|
374
|
-
return `Parameter "${key}" must be a boolean, got ${typeof value}`;
|
|
375
|
-
}
|
|
376
|
-
break;
|
|
377
|
-
case "array":
|
|
378
|
-
if (!Array.isArray(value)) {
|
|
379
|
-
return `Parameter "${key}" must be an array, got ${typeof value}`;
|
|
380
|
-
}
|
|
381
|
-
if (schema.items) {
|
|
382
|
-
for (let i = 0; i < value.length; i++) {
|
|
383
|
-
const itemError = this.validateParameterType(`${key}[${i}]`, value[i], schema.items);
|
|
384
|
-
if (itemError) {
|
|
385
|
-
return itemError;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
break;
|
|
390
|
-
case "object":
|
|
391
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
392
|
-
return `Parameter "${key}" must be an object, got ${typeof value}`;
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
if (schema.enum && schema.enum.length > 0) {
|
|
397
|
-
const enumValues = schema.enum;
|
|
398
|
-
let isValidEnum = false;
|
|
399
|
-
for (const enumValue of enumValues) {
|
|
400
|
-
if (value === enumValue) {
|
|
401
|
-
isValidEnum = true;
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (!isValidEnum) {
|
|
406
|
-
return `Parameter "${key}" must be one of: ${enumValues.join(", ")}, got ${value}`;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return void 0;
|
|
410
|
-
}
|
|
411
707
|
/**
|
|
412
708
|
* Validate constructor inputs
|
|
413
709
|
*/
|
|
@@ -451,6 +747,132 @@ function createZodFunctionTool(name, description, zodSchema, fn) {
|
|
|
451
747
|
|
|
452
748
|
// src/implementations/openapi-tool.ts
|
|
453
749
|
import { ToolExecutionError as ToolExecutionError2, ValidationError as ValidationError3 } from "@robota-sdk/agent-core";
|
|
750
|
+
|
|
751
|
+
// src/implementations/openapi-schema-converter.ts
|
|
752
|
+
var HTTP_METHODS = [
|
|
753
|
+
"get",
|
|
754
|
+
"post",
|
|
755
|
+
"put",
|
|
756
|
+
"delete",
|
|
757
|
+
"patch",
|
|
758
|
+
"head",
|
|
759
|
+
"options"
|
|
760
|
+
];
|
|
761
|
+
function findOperation(apiSpec, operationId) {
|
|
762
|
+
for (const [path, pathItem] of Object.entries(apiSpec.paths || {})) {
|
|
763
|
+
if (!pathItem) continue;
|
|
764
|
+
for (const method of HTTP_METHODS) {
|
|
765
|
+
const operation = pathItem[method];
|
|
766
|
+
if (operation?.operationId === operationId) {
|
|
767
|
+
return { method, path, operation };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return void 0;
|
|
772
|
+
}
|
|
773
|
+
function mapOpenAPIType(type) {
|
|
774
|
+
switch (type) {
|
|
775
|
+
case "string":
|
|
776
|
+
return "string";
|
|
777
|
+
case "number":
|
|
778
|
+
return "number";
|
|
779
|
+
case "integer":
|
|
780
|
+
return "integer";
|
|
781
|
+
case "boolean":
|
|
782
|
+
return "boolean";
|
|
783
|
+
case "array":
|
|
784
|
+
return "array";
|
|
785
|
+
case "object":
|
|
786
|
+
return "object";
|
|
787
|
+
default:
|
|
788
|
+
return "string";
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function convertOpenAPISchemaToParameterSchema(schema) {
|
|
792
|
+
if ("$ref" in schema) {
|
|
793
|
+
return { type: "object" };
|
|
794
|
+
}
|
|
795
|
+
const result = {
|
|
796
|
+
type: mapOpenAPIType(schema.type)
|
|
797
|
+
};
|
|
798
|
+
if (schema.description) {
|
|
799
|
+
result.description = schema.description;
|
|
800
|
+
}
|
|
801
|
+
if (schema.enum) {
|
|
802
|
+
result.enum = schema.enum;
|
|
803
|
+
}
|
|
804
|
+
if (schema.minimum !== void 0) {
|
|
805
|
+
result.minimum = schema.minimum;
|
|
806
|
+
}
|
|
807
|
+
if (schema.maximum !== void 0) {
|
|
808
|
+
result.maximum = schema.maximum;
|
|
809
|
+
}
|
|
810
|
+
if (schema.pattern) {
|
|
811
|
+
result.pattern = schema.pattern;
|
|
812
|
+
}
|
|
813
|
+
if (schema.format) {
|
|
814
|
+
result.format = schema.format;
|
|
815
|
+
}
|
|
816
|
+
if (schema.default !== void 0) {
|
|
817
|
+
result.default = schema.default;
|
|
818
|
+
}
|
|
819
|
+
if (schema.type === "array" && schema.items) {
|
|
820
|
+
result.items = convertOpenAPISchemaToParameterSchema(schema.items);
|
|
821
|
+
}
|
|
822
|
+
if (schema.type === "object" && schema.properties) {
|
|
823
|
+
result.properties = {};
|
|
824
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
825
|
+
result.properties[propName] = convertOpenAPISchemaToParameterSchema(propSchema);
|
|
826
|
+
}
|
|
827
|
+
if (schema.required && schema.required.length > 0) {
|
|
828
|
+
result.required = schema.required;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
function convertOpenAPIParamToSchema(param) {
|
|
834
|
+
const schema = param.schema;
|
|
835
|
+
return convertOpenAPISchemaToParameterSchema(schema);
|
|
836
|
+
}
|
|
837
|
+
function createSchemaFromOperation(operationId, opSpec) {
|
|
838
|
+
const properties = {};
|
|
839
|
+
const required = [];
|
|
840
|
+
const params = opSpec.parameters || [];
|
|
841
|
+
for (const param of params) {
|
|
842
|
+
properties[param.name] = convertOpenAPIParamToSchema(param);
|
|
843
|
+
if (param.required) {
|
|
844
|
+
required.push(param.name);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (opSpec.requestBody) {
|
|
848
|
+
const requestBody = opSpec.requestBody;
|
|
849
|
+
const jsonContent = requestBody.content?.["application/json"];
|
|
850
|
+
if (jsonContent?.schema) {
|
|
851
|
+
const bodySchema = convertOpenAPISchemaToParameterSchema(jsonContent.schema);
|
|
852
|
+
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
853
|
+
Object.assign(properties, bodySchema.properties);
|
|
854
|
+
const schemaWithRequired = bodySchema;
|
|
855
|
+
if (schemaWithRequired.required) {
|
|
856
|
+
required.push(...schemaWithRequired.required);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const schemaParams = {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties
|
|
864
|
+
};
|
|
865
|
+
if (required.length > 0) {
|
|
866
|
+
schemaParams.required = required;
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
name: operationId,
|
|
870
|
+
description: opSpec.summary || opSpec.description || `OpenAPI operation: ${operationId}`,
|
|
871
|
+
parameters: schemaParams
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/implementations/openapi-tool.ts
|
|
454
876
|
var OpenAPITool = class {
|
|
455
877
|
schema;
|
|
456
878
|
apiSpec;
|
|
@@ -557,36 +979,13 @@ var OpenAPITool = class {
|
|
|
557
979
|
* @private
|
|
558
980
|
*/
|
|
559
981
|
async executeAPICall(parameters, _context) {
|
|
560
|
-
const operation = this.
|
|
982
|
+
const operation = findOperation(this.apiSpec, this.operationId);
|
|
561
983
|
if (!operation) {
|
|
562
984
|
throw new Error(`Operation ${this.operationId} not found in OpenAPI spec`);
|
|
563
985
|
}
|
|
564
|
-
|
|
986
|
+
this.buildRequestConfig(operation, parameters);
|
|
565
987
|
throw new Error("Not implemented: actual API execution is not yet available");
|
|
566
988
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Find the operation in the OpenAPI specification
|
|
569
|
-
*/
|
|
570
|
-
findOperation() {
|
|
571
|
-
for (const [path, pathItem] of Object.entries(this.apiSpec.paths || {})) {
|
|
572
|
-
if (!pathItem) continue;
|
|
573
|
-
for (const method of [
|
|
574
|
-
"get",
|
|
575
|
-
"post",
|
|
576
|
-
"put",
|
|
577
|
-
"delete",
|
|
578
|
-
"patch",
|
|
579
|
-
"head",
|
|
580
|
-
"options"
|
|
581
|
-
]) {
|
|
582
|
-
const operation = pathItem[method];
|
|
583
|
-
if (operation?.operationId === this.operationId) {
|
|
584
|
-
return { method, path, operation };
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return void 0;
|
|
589
|
-
}
|
|
590
989
|
/**
|
|
591
990
|
* Build HTTP request configuration from OpenAPI operation and parameters
|
|
592
991
|
*/
|
|
@@ -658,121 +1057,13 @@ var OpenAPITool = class {
|
|
|
658
1057
|
* Create tool schema from OpenAPI operation specification
|
|
659
1058
|
*/
|
|
660
1059
|
createSchemaFromOpenAPI() {
|
|
661
|
-
const operation = this.
|
|
1060
|
+
const operation = findOperation(this.apiSpec, this.operationId);
|
|
662
1061
|
if (!operation) {
|
|
663
1062
|
throw new Error(
|
|
664
1063
|
`[STRICT-POLICY][EMITTER-CONTRACT] OpenAPI operation not found: ${this.operationId}. Emitter contract must provide a valid operationId present in the OpenAPI document.`
|
|
665
1064
|
);
|
|
666
1065
|
}
|
|
667
|
-
|
|
668
|
-
const properties = {};
|
|
669
|
-
const required = [];
|
|
670
|
-
const params = opSpec.parameters || [];
|
|
671
|
-
for (const param of params) {
|
|
672
|
-
properties[param.name] = this.convertOpenAPIParamToSchema(param);
|
|
673
|
-
if (param.required) {
|
|
674
|
-
required.push(param.name);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (opSpec.requestBody) {
|
|
678
|
-
const requestBody = opSpec.requestBody;
|
|
679
|
-
const jsonContent = requestBody.content?.["application/json"];
|
|
680
|
-
if (jsonContent?.schema) {
|
|
681
|
-
const bodySchema = this.convertOpenAPISchemaToParameterSchema(jsonContent.schema);
|
|
682
|
-
if (bodySchema.type === "object" && bodySchema.properties) {
|
|
683
|
-
Object.assign(properties, bodySchema.properties);
|
|
684
|
-
const schemaWithRequired = bodySchema;
|
|
685
|
-
if (schemaWithRequired.required) {
|
|
686
|
-
required.push(...schemaWithRequired.required);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const schemaParams = {
|
|
692
|
-
type: "object",
|
|
693
|
-
properties
|
|
694
|
-
};
|
|
695
|
-
if (required.length > 0) {
|
|
696
|
-
schemaParams.required = required;
|
|
697
|
-
}
|
|
698
|
-
return {
|
|
699
|
-
name: this.operationId,
|
|
700
|
-
description: opSpec.summary || opSpec.description || `OpenAPI operation: ${this.operationId}`,
|
|
701
|
-
parameters: schemaParams
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* Convert OpenAPI parameter to tool parameter schema
|
|
706
|
-
*/
|
|
707
|
-
convertOpenAPIParamToSchema(param) {
|
|
708
|
-
const schema = param.schema;
|
|
709
|
-
return this.convertOpenAPISchemaToParameterSchema(schema);
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Convert OpenAPI schema to parameter schema
|
|
713
|
-
*/
|
|
714
|
-
convertOpenAPISchemaToParameterSchema(schema) {
|
|
715
|
-
if ("$ref" in schema) {
|
|
716
|
-
return { type: "object" };
|
|
717
|
-
}
|
|
718
|
-
const result = {
|
|
719
|
-
type: this.mapOpenAPIType(schema.type)
|
|
720
|
-
};
|
|
721
|
-
if (schema.description) {
|
|
722
|
-
result.description = schema.description;
|
|
723
|
-
}
|
|
724
|
-
if (schema.enum) {
|
|
725
|
-
result.enum = schema.enum;
|
|
726
|
-
}
|
|
727
|
-
if (schema.minimum !== void 0) {
|
|
728
|
-
result.minimum = schema.minimum;
|
|
729
|
-
}
|
|
730
|
-
if (schema.maximum !== void 0) {
|
|
731
|
-
result.maximum = schema.maximum;
|
|
732
|
-
}
|
|
733
|
-
if (schema.pattern) {
|
|
734
|
-
result.pattern = schema.pattern;
|
|
735
|
-
}
|
|
736
|
-
if (schema.format) {
|
|
737
|
-
result.format = schema.format;
|
|
738
|
-
}
|
|
739
|
-
if (schema.default !== void 0) {
|
|
740
|
-
result.default = schema.default;
|
|
741
|
-
}
|
|
742
|
-
if (schema.type === "array" && schema.items) {
|
|
743
|
-
result.items = this.convertOpenAPISchemaToParameterSchema(schema.items);
|
|
744
|
-
}
|
|
745
|
-
if (schema.type === "object" && schema.properties) {
|
|
746
|
-
result.properties = {};
|
|
747
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
748
|
-
result.properties[propName] = this.convertOpenAPISchemaToParameterSchema(propSchema);
|
|
749
|
-
}
|
|
750
|
-
if (schema.required && schema.required.length > 0) {
|
|
751
|
-
result.required = schema.required;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
return result;
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Map OpenAPI type to JSON schema type
|
|
758
|
-
*/
|
|
759
|
-
mapOpenAPIType(type) {
|
|
760
|
-
switch (type) {
|
|
761
|
-
case "string":
|
|
762
|
-
return "string";
|
|
763
|
-
case "number":
|
|
764
|
-
return "number";
|
|
765
|
-
case "integer":
|
|
766
|
-
return "integer";
|
|
767
|
-
case "boolean":
|
|
768
|
-
return "boolean";
|
|
769
|
-
case "array":
|
|
770
|
-
return "array";
|
|
771
|
-
case "object":
|
|
772
|
-
return "object";
|
|
773
|
-
default:
|
|
774
|
-
return "string";
|
|
775
|
-
}
|
|
1066
|
+
return createSchemaFromOperation(this.operationId, operation.operation);
|
|
776
1067
|
}
|
|
777
1068
|
};
|
|
778
1069
|
function createOpenAPITool(config) {
|
|
@@ -788,9 +1079,33 @@ var BashSchema = z.object({
|
|
|
788
1079
|
timeout: z.number().optional().describe("Optional timeout in milliseconds (max 600000). Default is 120000 (2 minutes)"),
|
|
789
1080
|
workingDirectory: z.string().optional().describe("Working directory for the command. Defaults to the current working directory")
|
|
790
1081
|
});
|
|
791
|
-
async function runBash(args) {
|
|
1082
|
+
async function runBash(args, options = {}) {
|
|
792
1083
|
const { command, timeout = DEFAULT_TIMEOUT_MS, workingDirectory } = args;
|
|
793
|
-
|
|
1084
|
+
if (options.sandboxClient) {
|
|
1085
|
+
try {
|
|
1086
|
+
const sandboxResult = await options.sandboxClient.run(command, {
|
|
1087
|
+
timeoutMs: timeout,
|
|
1088
|
+
workingDirectory
|
|
1089
|
+
});
|
|
1090
|
+
const output = sandboxResult.stderr ? `${sandboxResult.stdout}
|
|
1091
|
+
stderr:
|
|
1092
|
+
${sandboxResult.stderr}` : sandboxResult.stdout;
|
|
1093
|
+
const result = {
|
|
1094
|
+
success: true,
|
|
1095
|
+
output,
|
|
1096
|
+
exitCode: sandboxResult.exitCode
|
|
1097
|
+
};
|
|
1098
|
+
return JSON.stringify(result);
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
const result = {
|
|
1101
|
+
success: false,
|
|
1102
|
+
output: "",
|
|
1103
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1104
|
+
};
|
|
1105
|
+
return JSON.stringify(result);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return new Promise((resolve4) => {
|
|
794
1109
|
const stdoutChunks = [];
|
|
795
1110
|
const stderrChunks = [];
|
|
796
1111
|
let timedOut = false;
|
|
@@ -809,12 +1124,17 @@ async function runBash(args) {
|
|
|
809
1124
|
const timer = setTimeout(() => {
|
|
810
1125
|
timedOut = true;
|
|
811
1126
|
child.kill("SIGTERM");
|
|
1127
|
+
settle({
|
|
1128
|
+
success: false,
|
|
1129
|
+
output: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1130
|
+
error: `Command timed out after ${timeout}ms`
|
|
1131
|
+
});
|
|
812
1132
|
}, timeout);
|
|
813
1133
|
function settle(result) {
|
|
814
1134
|
if (settled) return;
|
|
815
1135
|
settled = true;
|
|
816
1136
|
clearTimeout(timer);
|
|
817
|
-
|
|
1137
|
+
resolve4(JSON.stringify(result));
|
|
818
1138
|
}
|
|
819
1139
|
child.on("error", (err) => {
|
|
820
1140
|
settle({
|
|
@@ -847,17 +1167,20 @@ ${stderr}` : stdout;
|
|
|
847
1167
|
});
|
|
848
1168
|
});
|
|
849
1169
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1170
|
+
function createBashTool(options = {}) {
|
|
1171
|
+
return createZodFunctionTool(
|
|
1172
|
+
"Bash",
|
|
1173
|
+
"Executes a given bash command and returns its output.\n\nThe working directory persists between commands, but shell state does not.\n\nIMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or `echo` commands. Instead, use the appropriate dedicated tool:\n - File search: Use Glob (NOT find or ls)\n - Content search: Use Grep (NOT grep or rg)\n - Read files: Use Read (NOT cat/head/tail)\n - Edit files: Use Edit (NOT sed/awk)\n\nFor simple commands, keep the description brief (5-10 words). For complex commands, include enough context to clarify what the command does.\n\nOutput is limited to 30,000 characters. Longer output will be middle-truncated.",
|
|
1174
|
+
BashSchema,
|
|
1175
|
+
async (params) => {
|
|
1176
|
+
return runBash(params, options);
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
var bashTool = createBashTool();
|
|
858
1181
|
|
|
859
1182
|
// src/builtins/read-tool.ts
|
|
860
|
-
import { readFile, stat } from "fs/promises";
|
|
1183
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
861
1184
|
import { z as z2 } from "zod";
|
|
862
1185
|
var DEFAULT_LIMIT = 2e3;
|
|
863
1186
|
var ReadSchema = z2.object({
|
|
@@ -884,88 +1207,152 @@ function formatWithLineNumbers(lines, startLine) {
|
|
|
884
1207
|
return `${lineNum} ${line}`;
|
|
885
1208
|
}).join("\n");
|
|
886
1209
|
}
|
|
887
|
-
|
|
1210
|
+
function formatReadResult(filePath, content, startLine, limit) {
|
|
1211
|
+
const allLines = content.split("\n");
|
|
1212
|
+
if (allLines[allLines.length - 1] === "") {
|
|
1213
|
+
allLines.pop();
|
|
1214
|
+
}
|
|
1215
|
+
const zeroBasedStart = startLine - 1;
|
|
1216
|
+
const selectedLines = allLines.slice(zeroBasedStart, zeroBasedStart + limit);
|
|
1217
|
+
const output = formatWithLineNumbers(selectedLines, startLine);
|
|
1218
|
+
const totalLines = allLines.length;
|
|
1219
|
+
const returnedLines = selectedLines.length;
|
|
1220
|
+
const header = returnedLines < totalLines ? `[File: ${filePath} (lines ${startLine}-${startLine + returnedLines - 1} of ${totalLines})]
|
|
1221
|
+
` : `[File: ${filePath} (${totalLines} lines)]
|
|
1222
|
+
`;
|
|
1223
|
+
const result = {
|
|
1224
|
+
success: true,
|
|
1225
|
+
output: header + output
|
|
1226
|
+
};
|
|
1227
|
+
return JSON.stringify(result);
|
|
1228
|
+
}
|
|
1229
|
+
async function readFileTool(args, options = {}) {
|
|
888
1230
|
const { filePath, offset, limit = DEFAULT_LIMIT } = args;
|
|
889
1231
|
const startLine = offset !== void 0 && offset > 0 ? offset : 1;
|
|
1232
|
+
if (options.sandboxClient) {
|
|
1233
|
+
try {
|
|
1234
|
+
const content2 = await options.sandboxClient.readFile(filePath);
|
|
1235
|
+
return formatReadResult(filePath, content2, startLine, limit);
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
const result = {
|
|
1238
|
+
success: false,
|
|
1239
|
+
output: "",
|
|
1240
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1241
|
+
};
|
|
1242
|
+
return JSON.stringify(result);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
890
1245
|
let fileStats;
|
|
891
1246
|
try {
|
|
892
1247
|
fileStats = await stat(filePath);
|
|
893
1248
|
} catch (err) {
|
|
894
|
-
const
|
|
1249
|
+
const result = {
|
|
895
1250
|
success: false,
|
|
896
1251
|
output: "",
|
|
897
1252
|
error: `File not found: ${filePath}`
|
|
898
1253
|
};
|
|
899
|
-
return JSON.stringify(
|
|
1254
|
+
return JSON.stringify(result);
|
|
900
1255
|
}
|
|
901
1256
|
if (!fileStats.isFile()) {
|
|
902
|
-
const
|
|
1257
|
+
const result = {
|
|
903
1258
|
success: false,
|
|
904
1259
|
output: "",
|
|
905
1260
|
error: `Path is not a file: ${filePath}`
|
|
906
1261
|
};
|
|
907
|
-
return JSON.stringify(
|
|
1262
|
+
return JSON.stringify(result);
|
|
908
1263
|
}
|
|
909
1264
|
let buffer;
|
|
910
1265
|
try {
|
|
911
|
-
buffer = await
|
|
1266
|
+
buffer = await readFile2(filePath);
|
|
912
1267
|
} catch (err) {
|
|
913
|
-
const
|
|
1268
|
+
const result = {
|
|
914
1269
|
success: false,
|
|
915
1270
|
output: "",
|
|
916
1271
|
error: err instanceof Error ? err.message : String(err)
|
|
917
1272
|
};
|
|
918
|
-
return JSON.stringify(
|
|
1273
|
+
return JSON.stringify(result);
|
|
919
1274
|
}
|
|
920
1275
|
if (isBinary(buffer)) {
|
|
921
|
-
const
|
|
1276
|
+
const result = {
|
|
922
1277
|
success: false,
|
|
923
1278
|
output: "",
|
|
924
1279
|
error: `Binary file not supported: ${filePath}`
|
|
925
1280
|
};
|
|
926
|
-
return JSON.stringify(
|
|
1281
|
+
return JSON.stringify(result);
|
|
927
1282
|
}
|
|
928
1283
|
const content = buffer.toString("utf8");
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1284
|
+
return formatReadResult(filePath, content, startLine, limit);
|
|
1285
|
+
}
|
|
1286
|
+
function createReadTool(options = {}) {
|
|
1287
|
+
return createZodFunctionTool(
|
|
1288
|
+
"Read",
|
|
1289
|
+
"Reads a file from the local filesystem.\n\nBy default, reads up to 2000 lines from the beginning of the file. You can optionally specify offset and limit for partial reads.\n\nResults are returned using cat -n format, with line numbers starting at 1.\n\nThe file_path parameter must be an absolute path, not a relative path.",
|
|
1290
|
+
ReadSchema,
|
|
1291
|
+
async (params) => {
|
|
1292
|
+
return readFileTool(params, options);
|
|
1293
|
+
}
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
var readTool = createReadTool();
|
|
1297
|
+
|
|
1298
|
+
// src/builtins/write-tool.ts
|
|
1299
|
+
import { z as z3 } from "zod";
|
|
1300
|
+
|
|
1301
|
+
// src/builtins/atomic-file-write.ts
|
|
1302
|
+
import { randomBytes } from "crypto";
|
|
1303
|
+
import { chmod, mkdir, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
1304
|
+
import { basename, dirname, join as join2 } from "path";
|
|
1305
|
+
var TEMP_RANDOM_BYTES = 6;
|
|
1306
|
+
var PRESERVED_MODE_BITS = 4095;
|
|
1307
|
+
var MISSING_FILE_ERROR_CODE = "ENOENT";
|
|
1308
|
+
function createTempFilePath(filePath) {
|
|
1309
|
+
const dir = dirname(filePath);
|
|
1310
|
+
const name = basename(filePath);
|
|
1311
|
+
const suffix = randomBytes(TEMP_RANDOM_BYTES).toString("hex");
|
|
1312
|
+
return join2(dir, `.${name}.robota-tmp-${process.pid}-${Date.now()}-${suffix}`);
|
|
1313
|
+
}
|
|
1314
|
+
async function readExistingMode(filePath) {
|
|
1315
|
+
try {
|
|
1316
|
+
const fileStats = await stat2(filePath);
|
|
1317
|
+
return fileStats.mode & PRESERVED_MODE_BITS;
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (error instanceof Error && hasErrorCode(error, MISSING_FILE_ERROR_CODE)) return void 0;
|
|
1320
|
+
throw error;
|
|
932
1321
|
}
|
|
933
|
-
const zeroBasedStart = startLine - 1;
|
|
934
|
-
const selectedLines = allLines.slice(zeroBasedStart, zeroBasedStart + limit);
|
|
935
|
-
const output = formatWithLineNumbers(selectedLines, startLine);
|
|
936
|
-
const totalLines = allLines.length;
|
|
937
|
-
const returnedLines = selectedLines.length;
|
|
938
|
-
const header = returnedLines < totalLines ? `[File: ${filePath} (lines ${startLine}-${startLine + returnedLines - 1} of ${totalLines})]
|
|
939
|
-
` : `[File: ${filePath} (${totalLines} lines)]
|
|
940
|
-
`;
|
|
941
|
-
const result = {
|
|
942
|
-
success: true,
|
|
943
|
-
output: header + output
|
|
944
|
-
};
|
|
945
|
-
return JSON.stringify(result);
|
|
946
1322
|
}
|
|
947
|
-
|
|
948
|
-
"
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1323
|
+
function hasErrorCode(error, code) {
|
|
1324
|
+
return "code" in error && error.code === code;
|
|
1325
|
+
}
|
|
1326
|
+
async function atomicWriteUtf8File(filePath, content) {
|
|
1327
|
+
const dir = dirname(filePath);
|
|
1328
|
+
await mkdir(dir, { recursive: true });
|
|
1329
|
+
const existingMode = await readExistingMode(filePath);
|
|
1330
|
+
const tempFilePath = createTempFilePath(filePath);
|
|
1331
|
+
try {
|
|
1332
|
+
await writeFile(tempFilePath, content, "utf8");
|
|
1333
|
+
if (existingMode !== void 0) {
|
|
1334
|
+
await chmod(tempFilePath, existingMode);
|
|
1335
|
+
}
|
|
1336
|
+
await rename(tempFilePath, filePath);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
await rm(tempFilePath, { force: true }).catch(() => void 0);
|
|
1339
|
+
throw error;
|
|
953
1340
|
}
|
|
954
|
-
|
|
1341
|
+
}
|
|
955
1342
|
|
|
956
1343
|
// src/builtins/write-tool.ts
|
|
957
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
958
|
-
import { dirname } from "path";
|
|
959
|
-
import { z as z3 } from "zod";
|
|
960
1344
|
var WriteSchema = z3.object({
|
|
961
1345
|
filePath: z3.string().describe("The absolute path to the file to write"),
|
|
962
1346
|
content: z3.string().describe("The content to write to the file")
|
|
963
1347
|
});
|
|
964
|
-
async function writeFileTool(args) {
|
|
1348
|
+
async function writeFileTool(args, options = {}) {
|
|
965
1349
|
const { filePath, content } = args;
|
|
966
1350
|
try {
|
|
967
|
-
|
|
968
|
-
|
|
1351
|
+
if (options.sandboxClient) {
|
|
1352
|
+
await options.sandboxClient.writeFile(filePath, content);
|
|
1353
|
+
} else {
|
|
1354
|
+
await atomicWriteUtf8File(filePath, content);
|
|
1355
|
+
}
|
|
969
1356
|
const result = {
|
|
970
1357
|
success: true,
|
|
971
1358
|
output: `Written ${Buffer.byteLength(content, "utf8")} bytes to ${filePath}`
|
|
@@ -980,17 +1367,20 @@ async function writeFileTool(args) {
|
|
|
980
1367
|
return JSON.stringify(result);
|
|
981
1368
|
}
|
|
982
1369
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1370
|
+
function createWriteTool(options = {}) {
|
|
1371
|
+
return createZodFunctionTool(
|
|
1372
|
+
"Write",
|
|
1373
|
+
"Writes a file to the local filesystem. This will overwrite an existing file if one exists.\n\nALWAYS prefer the Edit tool for modifying existing files \u2014 it only sends the diff. Only use this tool to create new files or for complete rewrites.\n\nNEVER create documentation files (*.md) or README files unless explicitly requested by the user.",
|
|
1374
|
+
WriteSchema,
|
|
1375
|
+
async (params) => {
|
|
1376
|
+
return writeFileTool(params, options);
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
var writeTool = createWriteTool();
|
|
991
1381
|
|
|
992
1382
|
// src/builtins/edit-tool.ts
|
|
993
|
-
import { readFile as
|
|
1383
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
994
1384
|
import { z as z4 } from "zod";
|
|
995
1385
|
var EditSchema = z4.object({
|
|
996
1386
|
filePath: z4.string().describe("The absolute path to the file to modify"),
|
|
@@ -1000,11 +1390,11 @@ var EditSchema = z4.object({
|
|
|
1000
1390
|
"Replace all occurrences of old_string (default: false). Useful for renaming variables"
|
|
1001
1391
|
)
|
|
1002
1392
|
});
|
|
1003
|
-
async function editFileTool(args) {
|
|
1393
|
+
async function editFileTool(args, options = {}) {
|
|
1004
1394
|
const { filePath, oldString, newString, replaceAll = false } = args;
|
|
1005
1395
|
let content;
|
|
1006
1396
|
try {
|
|
1007
|
-
content = await
|
|
1397
|
+
content = options.sandboxClient ? await options.sandboxClient.readFile(filePath) : await readFile3(filePath, "utf8");
|
|
1008
1398
|
} catch (err) {
|
|
1009
1399
|
const result2 = {
|
|
1010
1400
|
success: false,
|
|
@@ -1036,7 +1426,11 @@ async function editFileTool(args) {
|
|
|
1036
1426
|
}
|
|
1037
1427
|
const updated = replaceAll ? content.split(oldString).join(newString) : content.slice(0, content.indexOf(oldString)) + newString + content.slice(content.indexOf(oldString) + oldString.length);
|
|
1038
1428
|
try {
|
|
1039
|
-
|
|
1429
|
+
if (options.sandboxClient) {
|
|
1430
|
+
await options.sandboxClient.writeFile(filePath, updated);
|
|
1431
|
+
} else {
|
|
1432
|
+
await atomicWriteUtf8File(filePath, updated);
|
|
1433
|
+
}
|
|
1040
1434
|
} catch (err) {
|
|
1041
1435
|
const result2 = {
|
|
1042
1436
|
success: false,
|
|
@@ -1046,24 +1440,30 @@ async function editFileTool(args) {
|
|
|
1046
1440
|
return JSON.stringify(result2);
|
|
1047
1441
|
}
|
|
1048
1442
|
const count = replaceAll ? content.split(oldString).length - 1 : 1;
|
|
1443
|
+
const matchIdx = content.indexOf(oldString);
|
|
1444
|
+
const startLine = matchIdx >= 0 ? content.substring(0, matchIdx).split("\n").length : 1;
|
|
1049
1445
|
const result = {
|
|
1050
1446
|
success: true,
|
|
1051
|
-
output: `Replaced ${count} occurrence(s) in ${filePath}
|
|
1447
|
+
output: `Replaced ${count} occurrence(s) in ${filePath}`,
|
|
1448
|
+
startLine
|
|
1052
1449
|
};
|
|
1053
1450
|
return JSON.stringify(result);
|
|
1054
1451
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1452
|
+
function createEditTool(options = {}) {
|
|
1453
|
+
return createZodFunctionTool(
|
|
1454
|
+
"Edit",
|
|
1455
|
+
"Performs exact string replacements in files.\n\nYou must use the Read tool at least once before editing. When editing text from Read output, preserve the exact indentation.\n\nThe edit will FAIL if old_string is not unique in the file. Either provide more surrounding context to make it unique, or use replace_all to change every instance.\n\nALWAYS prefer editing existing files over creating new ones.",
|
|
1456
|
+
EditSchema,
|
|
1457
|
+
async (params) => {
|
|
1458
|
+
return editFileTool(params, options);
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
var editTool = createEditTool();
|
|
1063
1463
|
|
|
1064
1464
|
// src/builtins/glob-tool.ts
|
|
1065
|
-
import { stat as
|
|
1066
|
-
import { resolve } from "path";
|
|
1465
|
+
import { stat as stat3 } from "fs/promises";
|
|
1466
|
+
import { resolve as resolve2 } from "path";
|
|
1067
1467
|
import fg from "fast-glob";
|
|
1068
1468
|
import { z as z5 } from "zod";
|
|
1069
1469
|
var DEFAULT_MAX_RESULTS = 1e3;
|
|
@@ -1078,7 +1478,7 @@ var GlobSchema = z5.object({
|
|
|
1078
1478
|
});
|
|
1079
1479
|
async function globFileTool(args) {
|
|
1080
1480
|
const { pattern, path: basePath } = args;
|
|
1081
|
-
const cwd = basePath ?
|
|
1481
|
+
const cwd = basePath ? resolve2(basePath) : process.cwd();
|
|
1082
1482
|
let matches;
|
|
1083
1483
|
try {
|
|
1084
1484
|
matches = await fg(pattern, {
|
|
@@ -1097,9 +1497,9 @@ async function globFileTool(args) {
|
|
|
1097
1497
|
}
|
|
1098
1498
|
const withMtime = await Promise.all(
|
|
1099
1499
|
matches.map(async (p) => {
|
|
1100
|
-
const absPath =
|
|
1500
|
+
const absPath = resolve2(cwd, p);
|
|
1101
1501
|
try {
|
|
1102
|
-
const s = await
|
|
1502
|
+
const s = await stat3(absPath);
|
|
1103
1503
|
return { path: p, mtime: s.mtimeMs };
|
|
1104
1504
|
} catch {
|
|
1105
1505
|
return { path: p, mtime: 0 };
|
|
@@ -1134,8 +1534,8 @@ var globTool = createZodFunctionTool(
|
|
|
1134
1534
|
);
|
|
1135
1535
|
|
|
1136
1536
|
// src/builtins/grep-tool.ts
|
|
1137
|
-
import { readFile as
|
|
1138
|
-
import { join, resolve as
|
|
1537
|
+
import { readFile as readFile4, readdir as readdir2, stat as stat4 } from "fs/promises";
|
|
1538
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
1139
1539
|
import { z as z6 } from "zod";
|
|
1140
1540
|
var GrepSchema = z6.object({
|
|
1141
1541
|
pattern: z6.string().describe("The regular expression pattern to search for in file contents"),
|
|
@@ -1163,16 +1563,16 @@ async function collectFiles(dirPath, glob) {
|
|
|
1163
1563
|
async function walk(current) {
|
|
1164
1564
|
let entryNames;
|
|
1165
1565
|
try {
|
|
1166
|
-
entryNames = await
|
|
1566
|
+
entryNames = await readdir2(current);
|
|
1167
1567
|
} catch {
|
|
1168
1568
|
return;
|
|
1169
1569
|
}
|
|
1170
1570
|
for (const name of entryNames) {
|
|
1171
1571
|
if (name === "node_modules" || name === ".git") continue;
|
|
1172
|
-
const fullPath =
|
|
1572
|
+
const fullPath = join3(current, name);
|
|
1173
1573
|
let fileStat;
|
|
1174
1574
|
try {
|
|
1175
|
-
fileStat = await
|
|
1575
|
+
fileStat = await stat4(fullPath);
|
|
1176
1576
|
} catch {
|
|
1177
1577
|
continue;
|
|
1178
1578
|
}
|
|
@@ -1228,7 +1628,7 @@ async function grepFileTool(args) {
|
|
|
1228
1628
|
contextLines = 0,
|
|
1229
1629
|
outputMode = "files_with_matches"
|
|
1230
1630
|
} = args;
|
|
1231
|
-
const targetPath = searchPath ?
|
|
1631
|
+
const targetPath = searchPath ? resolve3(searchPath) : process.cwd();
|
|
1232
1632
|
let regex;
|
|
1233
1633
|
try {
|
|
1234
1634
|
regex = new RegExp(pattern);
|
|
@@ -1242,7 +1642,7 @@ async function grepFileTool(args) {
|
|
|
1242
1642
|
}
|
|
1243
1643
|
let targetStat;
|
|
1244
1644
|
try {
|
|
1245
|
-
targetStat = await
|
|
1645
|
+
targetStat = await stat4(targetPath);
|
|
1246
1646
|
} catch {
|
|
1247
1647
|
const result2 = {
|
|
1248
1648
|
success: false,
|
|
@@ -1261,7 +1661,7 @@ async function grepFileTool(args) {
|
|
|
1261
1661
|
for (const filePath of files) {
|
|
1262
1662
|
let content;
|
|
1263
1663
|
try {
|
|
1264
|
-
const buffer = await
|
|
1664
|
+
const buffer = await readFile4(filePath);
|
|
1265
1665
|
const checkLen = Math.min(buffer.length, 8192);
|
|
1266
1666
|
let hasBinary = false;
|
|
1267
1667
|
for (let i = 0; i < checkLen; i++) {
|
|
@@ -1425,17 +1825,25 @@ var webSearchTool = createZodFunctionTool(
|
|
|
1425
1825
|
async (params) => runWebSearch(params)
|
|
1426
1826
|
);
|
|
1427
1827
|
export {
|
|
1828
|
+
E2BSandboxClient,
|
|
1428
1829
|
FunctionTool,
|
|
1830
|
+
InMemorySandboxClient,
|
|
1429
1831
|
OpenAPITool,
|
|
1430
1832
|
ToolRegistry,
|
|
1833
|
+
applyWorkspaceManifest,
|
|
1431
1834
|
bashTool,
|
|
1835
|
+
createBashTool,
|
|
1836
|
+
createEditTool,
|
|
1432
1837
|
createFunctionTool,
|
|
1433
1838
|
createOpenAPITool,
|
|
1839
|
+
createReadTool,
|
|
1840
|
+
createWriteTool,
|
|
1434
1841
|
createZodFunctionTool,
|
|
1435
1842
|
editTool,
|
|
1436
1843
|
globTool,
|
|
1437
1844
|
grepTool,
|
|
1438
1845
|
readTool,
|
|
1846
|
+
validateWorkspaceManifestPath,
|
|
1439
1847
|
webFetchTool,
|
|
1440
1848
|
webSearchTool,
|
|
1441
1849
|
writeTool,
|