Skip to content

Commit 26e23c5

Browse files
feat(openai): add support for local shell tool
1 parent 522b5af commit 26e23c5

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed

.changeset/moody-eels-pump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/openai": minor
3+
---
4+
5+
feat(openai): add support for local shell tool

libs/providers/langchain-openai/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,50 @@ const response = await llmWithComputer.invoke(
464464

465465
For more information, see [OpenAI's Computer Use Documentation](https://platform.openai.com/docs/guides/tools-computer-use).
466466

467+
### Local Shell Tool
468+
469+
The Local Shell tool allows models to run shell commands locally on a machine you provide. Commands are executed inside your own runtime—the API only returns the instructions.
470+
471+
> **Security Warning**: Running arbitrary shell commands can be dangerous. Always sandbox execution or add strict allow/deny-lists before forwarding commands to the system shell.
472+
> **Note**: This tool is designed to work with [Codex CLI](https://github.com/openai/codex) and the `codex-mini-latest` model.
473+
474+
```typescript
475+
import { ChatOpenAI, tools } from "@langchain/openai";
476+
import { exec } from "child_process";
477+
import { promisify } from "util";
478+
479+
const execAsync = promisify(exec);
480+
const model = new ChatOpenAI({ model: "codex-mini-latest" });
481+
482+
// With execute callback for automatic command handling
483+
const shell = tools.localShell({
484+
execute: async (action) => {
485+
const { command, env, working_directory, timeout_ms } = action;
486+
const result = await execAsync(command.join(" "), {
487+
cwd: working_directory ?? process.cwd(),
488+
env: { ...process.env, ...env },
489+
timeout: timeout_ms ?? undefined,
490+
});
491+
return result.stdout + result.stderr;
492+
},
493+
});
494+
495+
const llmWithShell = model.bindTools([shell]);
496+
const response = await llmWithShell.invoke(
497+
"List files in the current directory"
498+
);
499+
```
500+
501+
**Action properties**: The model returns actions with these properties:
502+
503+
- `command` - Array of argv tokens to execute
504+
- `env` - Environment variables to set
505+
- `working_directory` - Directory to run the command in
506+
- `timeout_ms` - Suggested timeout (enforce your own limits)
507+
- `user` - Optional user to run the command as
508+
509+
For more information, see [OpenAI's Local Shell Documentation](https://platform.openai.com/docs/guides/tools-local-shell).
510+
467511
## Embeddings
468512

469513
This package also adds support for OpenAI's embeddings model.

libs/providers/langchain-openai/src/tools/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,19 @@ export type {
6161
ComputerUseWaitAction,
6262
} from "./computerUse.js";
6363

64+
import { localShell } from "./localShell.js";
65+
export type {
66+
LocalShellTool,
67+
LocalShellOptions,
68+
LocalShellAction,
69+
} from "./localShell.js";
70+
6471
export const tools = {
6572
webSearch,
6673
mcp,
6774
codeInterpreter,
6875
fileSearch,
6976
imageGeneration,
7077
computerUse,
78+
localShell,
7179
};
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { OpenAI as OpenAIClient } from "openai";
2+
import { tool } from "@langchain/core/tools";
3+
4+
/**
5+
* Re-export action type from OpenAI SDK for convenience.
6+
* The action contains command details like argv tokens, environment variables,
7+
* working directory, timeout, and user.
8+
*/
9+
export type LocalShellAction =
10+
OpenAIClient.Responses.ResponseOutputItem.LocalShellCall.Action;
11+
12+
/**
13+
* Options for the Local Shell tool.
14+
*/
15+
export interface LocalShellOptions {
16+
/**
17+
* Optional execute function that handles shell command execution.
18+
* This function receives the action input and should return the command output
19+
* (stdout + stderr combined).
20+
*
21+
* If not provided, you'll need to handle action execution manually by
22+
* checking `local_shell_call` outputs in the response.
23+
*
24+
* @example
25+
* ```typescript
26+
* execute: async (action) => {
27+
* const result = await exec(action.command.join(' '), {
28+
* cwd: action.working_directory,
29+
* env: { ...process.env, ...action.env },
30+
* timeout: action.timeout_ms,
31+
* });
32+
* return result.stdout + result.stderr;
33+
* }
34+
* ```
35+
*/
36+
execute: (action: LocalShellAction) => string | Promise<string>;
37+
}
38+
39+
/**
40+
* OpenAI Local Shell tool type for the Responses API.
41+
*/
42+
export type LocalShellTool = OpenAIClient.Responses.Tool.LocalShell;
43+
44+
const TOOL_NAME = "local_shell";
45+
46+
/**
47+
* Creates a Local Shell tool that allows models to run shell commands locally
48+
* on a machine you provide. Commands are executed inside your own runtime—
49+
* the API only returns the instructions, but does not execute them on OpenAI infrastructure.
50+
*
51+
* **Important**: The local shell tool is designed to work with
52+
* [Codex CLI](https://github.com/openai/codex) and the `codex-mini-latest` model.
53+
*
54+
* **How it works**:
55+
* The tool operates in a continuous loop:
56+
* 1. Model sends shell commands (`local_shell_call` with `exec` action)
57+
* 2. Your code executes the command locally
58+
* 3. You return the output back to the model
59+
* 4. Repeat until the task is complete
60+
*
61+
* **Security Warning**: Running arbitrary shell commands can be dangerous.
62+
* Always sandbox execution or add strict allow/deny-lists before forwarding
63+
* a command to the system shell.
64+
*
65+
* @see {@link https://platform.openai.com/docs/guides/tools-local-shell | OpenAI Local Shell Documentation}
66+
*
67+
* @param options - Optional configuration for the Local Shell tool
68+
* @returns A Local Shell tool that can be passed to `bindTools`
69+
*
70+
* @example
71+
* ```typescript
72+
* import { ChatOpenAI, tools } from "@langchain/openai";
73+
* import { exec } from "child_process";
74+
* import { promisify } from "util";
75+
*
76+
* const execAsync = promisify(exec);
77+
* const model = new ChatOpenAI({ model: "codex-mini-latest" });
78+
*
79+
* // With execute callback for automatic command handling
80+
* const shell = tools.localShell({
81+
* execute: async (action) => {
82+
* const { command, env, working_directory, timeout_ms } = action;
83+
* const result = await execAsync(command.join(' '), {
84+
* cwd: working_directory ?? process.cwd(),
85+
* env: { ...process.env, ...env },
86+
* timeout: timeout_ms ?? undefined,
87+
* });
88+
* return result.stdout + result.stderr;
89+
* },
90+
* });
91+
*
92+
* const llmWithShell = model.bindTools([shell]);
93+
* const response = await llmWithShell.invoke(
94+
* "List files in the current directory"
95+
* );
96+
* ```
97+
*
98+
* @example
99+
* ```typescript
100+
* // Without execute callback (manual handling)
101+
* const shell = tools.localShell();
102+
*
103+
* const response = await model.invoke("List files", {
104+
* tools: [shell],
105+
* });
106+
*
107+
* // Access the shell call from the response
108+
* const shellCall = response.additional_kwargs.tool_outputs?.find(
109+
* (output) => output.type === "local_shell_call"
110+
* );
111+
* if (shellCall) {
112+
* console.log("Command to execute:", shellCall.action.command);
113+
* // Execute the command manually, then send back the output
114+
* }
115+
* ```
116+
*
117+
* @example
118+
* ```typescript
119+
* // Full shell loop example
120+
* async function shellLoop(model, task) {
121+
* let response = await model.invoke(task, {
122+
* tools: [tools.localShell()],
123+
* });
124+
*
125+
* while (true) {
126+
* const shellCall = response.additional_kwargs.tool_outputs?.find(
127+
* (output) => output.type === "local_shell_call"
128+
* );
129+
*
130+
* if (!shellCall) break;
131+
*
132+
* // Execute command (with proper sandboxing!)
133+
* const output = await executeCommand(shellCall.action);
134+
*
135+
* // Send output back to model
136+
* response = await model.invoke([
137+
* response,
138+
* {
139+
* type: "local_shell_call_output",
140+
* id: shellCall.call_id,
141+
* output: output,
142+
* },
143+
* ], {
144+
* tools: [tools.localShell()],
145+
* });
146+
* }
147+
*
148+
* return response;
149+
* }
150+
* ```
151+
*
152+
* @remarks
153+
* - Only available through the Responses API (not Chat Completions)
154+
* - Designed for use with `codex-mini-latest` model
155+
* - Commands are provided as argv tokens in `action.command`
156+
* - Action includes: `command`, `env`, `working_directory`, `timeout_ms`, `user`
157+
* - Always sandbox or validate commands before execution
158+
* - The `timeout_ms` from the model is only a hint—enforce your own limits
159+
*/
160+
export function localShell(options: LocalShellOptions) {
161+
const shellTool = tool(options.execute, {
162+
name: TOOL_NAME,
163+
description:
164+
"Execute shell commands locally on the machine. Commands are provided as argv tokens.",
165+
schema: {
166+
type: "object",
167+
properties: {
168+
action: {
169+
type: "string",
170+
enum: ["exec"],
171+
},
172+
},
173+
required: ["action"],
174+
},
175+
});
176+
177+
shellTool.extras = {
178+
...(shellTool.extras ?? {}),
179+
providerToolDefinition: {
180+
type: "local_shell",
181+
} as LocalShellTool,
182+
};
183+
184+
return shellTool;
185+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expectTypeOf, it, describe } from "vitest";
2+
import { tools } from "../index.js";
3+
4+
describe("OpenAI Local Shell Tool Tests", () => {
5+
it("localShell creates valid tool definitions", () => {
6+
tools.localShell({
7+
execute: async (cmd) => {
8+
expectTypeOf(cmd.command).toEqualTypeOf<string[]>();
9+
return "output";
10+
},
11+
});
12+
});
13+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect, it, describe } from "vitest";
2+
import { tools } from "../index.js";
3+
4+
describe("OpenAI Local Shell Tool Tests", () => {
5+
it("localShell creates valid tool definitions", () => {
6+
const shell = tools.localShell({
7+
execute: async () => "output",
8+
});
9+
10+
expect(shell.name).toBe("local_shell");
11+
expect(shell.extras?.providerToolDefinition).toMatchObject({
12+
type: "local_shell",
13+
});
14+
});
15+
});

0 commit comments

Comments
 (0)