Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/agents",
"version": "3.2.38",
"version": "3.2.41",
"main": "./dist/cjs/main.cjs",
"module": "./dist/esm/main.mjs",
"types": "./dist/types/index.d.ts",
Expand Down
24 changes: 12 additions & 12 deletions src/hooks/__tests__/createWorkspacePolicyHook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('createWorkspacePolicyHook', () => {
const hook = createWorkspacePolicyHook({ root: workspace });
const out = await call(
hook,
makeInput('read_file', { file_path: 'src/x.ts' })
makeInput('read_file', { path: 'src/x.ts' })
);
expect(out.decision).toBe('allow');
});
Expand All @@ -60,7 +60,7 @@ describe('createWorkspacePolicyHook', () => {
const hook = createWorkspacePolicyHook({ root: workspace });
const out = await call(
hook,
makeInput('read_file', { file_path: join(workspace, 'src/x.ts') })
makeInput('read_file', { path: join(workspace, 'src/x.ts') })
);
expect(out.decision).toBe('allow');
});
Expand All @@ -72,7 +72,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: join(extra, 'lib/y.ts') })
makeInput('read_file', { path: join(extra, 'lib/y.ts') })
);
expect(out.decision).toBe('allow');
});
Expand All @@ -81,7 +81,7 @@ describe('createWorkspacePolicyHook', () => {
const hook = createWorkspacePolicyHook({ root: workspace });
const out = await call(
hook,
makeInput('read_file', { file_path: '/etc/passwd' })
makeInput('read_file', { path: '/etc/passwd' })
);
expect(out.decision).toBe('ask');
expect(out.reason).toContain('/etc/passwd');
Expand All @@ -93,7 +93,7 @@ describe('createWorkspacePolicyHook', () => {
const out = await call(
hook,
makeInput('write_file', {
file_path: '/etc/foo',
path: '/etc/foo',
content: 'malicious',
})
);
Expand All @@ -107,7 +107,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: '/etc/passwd' })
makeInput('read_file', { path: '/etc/passwd' })
);
expect(out.decision).toBe('deny');
expect(out.reason).toContain('/etc/passwd');
Expand All @@ -121,7 +121,7 @@ describe('createWorkspacePolicyHook', () => {
const out = await call(
hook,
makeInput('edit_file', {
file_path: '/etc/foo',
path: '/etc/foo',
old_text: 'a',
new_text: 'b',
})
Expand All @@ -136,7 +136,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: '/etc/passwd' })
makeInput('read_file', { path: '/etc/passwd' })
);
expect(out.decision).toBe('allow');
});
Expand Down Expand Up @@ -184,7 +184,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: '/somewhere/else.ts' })
makeInput('read_file', { path: '/somewhere/else.ts' })
);
expect(out.reason).toBe('read_file blocked from /somewhere/else.ts');
});
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('createWorkspacePolicyHook', () => {
const hook = createWorkspacePolicyHook({ root: '.' });
const out = await call(
hook,
makeInput('read_file', { file_path: resolve('.', 'src/x.ts') })
makeInput('read_file', { path: resolve('.', 'src/x.ts') })
);
expect(out.decision).toBe('allow');
});
Expand All @@ -228,7 +228,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: 'escape' })
makeInput('read_file', { path: 'escape' })
);
expect(out.decision).toBe('deny');
expect(out.reason).toContain('escape');
Expand All @@ -245,7 +245,7 @@ describe('createWorkspacePolicyHook', () => {
});
const out = await call(
hook,
makeInput('read_file', { file_path: join(altMount, 'file.ts') })
makeInput('read_file', { path: join(altMount, 'file.ts') })
);
expect(out.decision).toBe('allow');
});
Expand Down
13 changes: 7 additions & 6 deletions src/hooks/createWorkspacePolicyHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,14 @@ function extractCompileCheckPaths(input: Record<string, unknown>): string[] {
return out;
}

// All built-in coding tools take their file/dir target in `path`.
const extractPath: PathExtractor = (i) =>
typeof i.path === 'string' && i.path !== '' ? [i.path] : [];

const DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {
[Constants.READ_FILE]: (i) =>
typeof i.file_path === 'string' ? [i.file_path] : [],
[Constants.WRITE_FILE]: (i) =>
typeof i.file_path === 'string' ? [i.file_path] : [],
[Constants.EDIT_FILE]: (i) =>
typeof i.file_path === 'string' ? [i.file_path] : [],
[Constants.READ_FILE]: extractPath,
[Constants.WRITE_FILE]: extractPath,
[Constants.EDIT_FILE]: extractPath,
[Constants.GREP_SEARCH]: (i) =>
typeof i.path === 'string' && i.path !== '' ? [i.path] : [],
[Constants.GLOB_SEARCH]: (i) =>
Expand Down
4 changes: 2 additions & 2 deletions src/tools/ReadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ CONSTRAINTS:
export const ReadFileToolSchema = {
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description:
'Path to the file. For skill files: "{skillName}/{path}" (e.g. "pdf-analyzer/src/utils.py"). For code execution output: the path as returned by the execution tool.',
},
},
required: ['file_path'],
required: ['path'],
} as const;

export const ReadFileToolDefinition = {
Expand Down
50 changes: 25 additions & 25 deletions src/tools/__tests__/LocalExecutionTools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ describe('local execution tools', () => {
aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
lang: 'py',
code: [
'await write_file(file_path="ptc.txt", content="from local ptc")',
'contents = await read_file(file_path="ptc.txt")',
'await write_file(path="ptc.txt", content="from local ptc")',
'contents = await read_file(path="ptc.txt")',
'print(contents)',
].join('\n'),
}),
Expand Down Expand Up @@ -205,8 +205,8 @@ describe('local execution tools', () => {
messages: [
aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
code: [
'write_file \'{"file_path":"bash-ptc.txt","content":"from bash ptc"}\'',
'read_file \'{"file_path":"bash-ptc.txt"}\'',
'write_file \'{"path":"bash-ptc.txt","content":"from bash ptc"}\'',
'read_file \'{"path":"bash-ptc.txt"}\'',
].join('\n'),
}),
],
Expand Down Expand Up @@ -335,8 +335,8 @@ describe('LocalFileCheckpointer', () => {

const writeTool = bundle.tools.find((tool_) => tool_.name === 'write_file');
expect(writeTool).toBeDefined();
await writeTool!.invoke({ file_path: 'cp.txt', content: 'first' });
await writeTool!.invoke({ file_path: 'cp.txt', content: 'second' });
await writeTool!.invoke({ path: 'cp.txt', content: 'first' });
await writeTool!.invoke({ path: 'cp.txt', content: 'second' });

const restored = await bundle.checkpointer!.rewind();
expect(restored).toBe(1);
Expand All @@ -352,7 +352,7 @@ describe('local read tool guards', () => {

const bundle = createLocalCodingToolBundle({ cwd });
const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
const result = await readTool!.invoke({ file_path: 'binary.bin' });
const result = await readTool!.invoke({ path: 'binary.bin' });
expect(String(result)).toContain('binary file');
});

Expand All @@ -366,7 +366,7 @@ describe('local read tool guards', () => {
maxReadBytes: 1024,
});
const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
const result = await readTool!.invoke({ file_path: 'big.txt' });
const result = await readTool!.invoke({ path: 'big.txt' });
expect(String(result)).toContain('exceeds the 1024-byte read cap');
});

Expand All @@ -380,7 +380,7 @@ describe('local read tool guards', () => {
const bundle = createLocalCodingToolBundle({ cwd });
const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
await expect(
readTool!.invoke({ file_path: 'escape/secret.txt' })
readTool!.invoke({ path: 'escape/secret.txt' })
).rejects.toThrow(/symlink escape/);
});
});
Expand All @@ -406,7 +406,7 @@ describe('local programmatic bridge auth', () => {
code: [
'import os, json, urllib.request, urllib.error',
'url = os.environ["BRIDGE_PROBE_URL"] if "BRIDGE_PROBE_URL" in os.environ else __LIBRECHAT_TOOL_BRIDGE',
'body = json.dumps({"name":"read_file","input":{"file_path":"x"}}).encode("utf-8")',
'body = json.dumps({"name":"read_file","input":{"path":"x"}}).encode("utf-8")',
'try:',
' req = urllib.request.Request(url, data=body, headers={"Content-Type":"application/json"}, method="POST")',
' urllib.request.urlopen(req, timeout=5)',
Expand Down Expand Up @@ -438,7 +438,7 @@ describe('local edit fuzzy matching', () => {
const bundle = createLocalCodingToolBundle({ cwd });
const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
const result = await editTool!.invoke({
file_path: 'a.ts',
path: 'a.ts',
// LLM emits a trailing-whitespace-stripped version.
old_text:
'function greet(name: string) {\n return `Hello, ${name}!`;\n}',
Expand All @@ -461,7 +461,7 @@ describe('local edit fuzzy matching', () => {
const bundle = createLocalCodingToolBundle({ cwd });
const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
const result = await editTool!.invoke({
file_path: 'a.ts',
path: 'a.ts',
// LLM stripped the 4-space indent
old_text: 'method() {\n return 1;\n}',
new_text: 'method() {\n return 42;\n}',
Expand All @@ -480,7 +480,7 @@ describe('local edit fuzzy matching', () => {
const bundle = createLocalCodingToolBundle({ cwd });
const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
const result = await editTool!.invoke({
file_path: 'a.txt',
path: 'a.txt',
old_text: 'second',
new_text: 'SECOND',
});
Expand All @@ -497,7 +497,7 @@ describe('local edit fuzzy matching', () => {
const bundle = createLocalCodingToolBundle({ cwd });
const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
await editTool!.invoke({
file_path: 'a.txt',
path: 'a.txt',
old_text: 'two',
new_text: 'TWO',
});
Expand All @@ -512,7 +512,7 @@ describe('local edit fuzzy matching', () => {
await fsWriteFile(file, BOM + 'hello\n', 'utf8');
const bundle = createLocalCodingToolBundle({ cwd });
const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
await writeTool!.invoke({ file_path: 'a.txt', content: 'goodbye\n' });
await writeTool!.invoke({ path: 'a.txt', content: 'goodbye\n' });
const raw = await fsReadFile(file, 'utf8');
expect(raw.startsWith(BOM)).toBe(true);
expect(raw.slice(1)).toBe('goodbye\n');
Expand All @@ -532,7 +532,7 @@ describe('local read attachments', () => {
await fsWriteFile(file, tinyPng);
const bundle = createLocalCodingToolBundle({ cwd });
const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
const result = await readTool!.invoke({ file_path: 'tiny.png' });
const result = await readTool!.invoke({ path: 'tiny.png' });
expect(String(result)).toContain('binary file');
});

Expand All @@ -552,7 +552,7 @@ describe('local read attachments', () => {
const message = (await readTool!.invoke({
id: 'call_image',
name: Constants.READ_FILE,
args: { file_path: 'tiny.png' },
args: { path: 'tiny.png' },
type: 'tool_call',
})) as { content: unknown; artifact: unknown };
expect(Array.isArray(message.content)).toBe(true);
Expand Down Expand Up @@ -589,7 +589,7 @@ describe('local read attachments', () => {
maxAttachmentBytes: 100,
});
const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
const result = await readTool!.invoke({ file_path: 'big.png' });
const result = await readTool!.invoke({ path: 'big.png' });
expect(String(result)).toMatch(/Refusing to embed/);
});

Expand All @@ -602,7 +602,7 @@ describe('local read attachments', () => {
attachReadAttachments: 'images-only',
});
const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
const result = await readTool!.invoke({ file_path: 'a.txt' });
const result = await readTool!.invoke({ path: 'a.txt' });
expect(String(result)).toContain('hello world');
});
});
Expand Down Expand Up @@ -662,7 +662,7 @@ describe('post-edit syntax check', () => {
const message = (await writeTool!.invoke({
id: 'call_w',
name: 'write_file',
args: { file_path: 'broken.js', content: 'function (\n' },
args: { path: 'broken.js', content: 'function (\n' },
type: 'tool_call',
})) as { content: string; artifact: { syntax_error?: string } };
expect(message.content).toContain('[syntax-check warning');
Expand All @@ -680,7 +680,7 @@ describe('post-edit syntax check', () => {
writeTool!.invoke({
id: 'call_w',
name: 'write_file',
args: { file_path: 'broken.js', content: 'function (\n' },
args: { path: 'broken.js', content: 'function (\n' },
type: 'tool_call',
})
).rejects.toThrow(/syntax check failed/);
Expand Down Expand Up @@ -850,7 +850,7 @@ describe('codex review fixes', () => {
const result = await readTool!.invoke({
id: 'c',
name: Constants.READ_FILE,
args: { file_path: join(parent, 'shared/lib.ts') },
args: { path: join(parent, 'shared/lib.ts') },
type: 'tool_call',
});
expect(JSON.stringify(result)).toContain('X');
Expand Down Expand Up @@ -2253,7 +2253,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
writeTool!.invoke({
id: 'wf-strict',
name: Constants.WRITE_FILE,
args: { file_path: file, content: 'function broken( {\n' },
args: { path: file, content: 'function broken( {\n' },
type: 'tool_call',
})
).rejects.toThrow(/syntax check failed.*reverted/i);
Expand Down Expand Up @@ -2281,7 +2281,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
writeTool!.invoke({
id: 'wf-strict-new',
name: Constants.WRITE_FILE,
args: { file_path: file, content: 'function broken( {\n' },
args: { path: file, content: 'function broken( {\n' },
type: 'tool_call',
})
).rejects.toThrow(/syntax check failed.*reverted/i);
Expand All @@ -2308,7 +2308,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
id: 'ef-strict',
name: Constants.EDIT_FILE,
args: {
file_path: file,
path: file,
old_text: 'return 1;',
new_text: 'return broken(',
},
Expand Down
Loading
Loading