🛻 feat: Route Bash Through Attached Workspaces (#15546)

* feat(code): Route bash through attached workspaces

* fix(code): harden attached bash schema adapter
This commit is contained in:
Danny Avila 2026-09-04 11:49:51 -04:00 • committed by GitHub
parent 2656f44c03
commit fddf7e31f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 485 additions and 18 deletions

View file

@ -45,6 +45,7 @@ const {
AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE,
isFatalAgentInitializationError,
codeExecutionAuthHeaders,
createAttachedWorkspaceBashTool,
resolveCodeExecutionContext,
resolveCallerCapabilityProjectionSnapshot,
LIST_WORKSPACE_FILES_TOOL_NAME,
@ -2122,14 +2123,21 @@ async function loadToolsForExecution({
}
if (isBashTool) {
try {
const bashTool = createBashExecutionTool({
authHeaders: () =>
codeExecutionAuthHeaders(
(bridgeWorkerId) => getCodeApiAuthHeaders(req, bridgeWorkerId),
codeExecutionContext,
),
...codeExecutionContext,
});
const authHeaders = () =>
codeExecutionAuthHeaders(
(bridgeWorkerId) => getCodeApiAuthHeaders(req, bridgeWorkerId),
codeExecutionContext,
);
const bashTool =
codeExecutionContext.environmentType === 'attached'
? createAttachedWorkspaceBashTool({
authHeaders,
baseUrl: codeExecutionContext.baseUrl,
})
: createBashExecutionTool({
authHeaders,
...codeExecutionContext,
});
allLoadedTools.push(bashTool);
} catch (error) {
logger.error(

View file

@ -18,6 +18,7 @@ const mockGetMCPServerTools = jest.fn();
const mockGetCachedTools = jest.fn();
const mockSendEvent = jest.fn();
const mockEmitChunk = jest.fn();
const mockCreateAttachedWorkspaceBashTool = jest.fn(() => ({ name: AgentConstants.BASH_TOOL }));
const mockResolveCodeExecutionContext = jest.fn(
({ statefulSessions, environment, userId, agentId, conversationId }) => {
if (!statefulSessions) {
@ -80,6 +81,7 @@ jest.mock('@librechat/api', () => ({
emitChunk: (...args) => mockEmitChunk(...args),
},
resolveCodeExecutionContext: (...args) => mockResolveCodeExecutionContext(...args),
createAttachedWorkspaceBashTool: (...args) => mockCreateAttachedWorkspaceBashTool(...args),
}));
const mockLoadToolsUtil = jest.fn();
@ -2631,6 +2633,47 @@ describe('ToolService - Action Capability Gating', () => {
}
});
it('loads bash execution through the attached worker transport', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.execute_code,
AgentCapabilities.stateful_code_sessions,
];
const req = createMockReq(capabilities);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockResolveCodeExecutionContext.mockReturnValueOnce({
baseUrl: 'http://attached-code.test/v1',
codeSessionKey: 'execute_code:stateful:attached',
executionProfile: 'stateful',
statefulSessions: true,
environmentType: 'attached',
bridgeWorkerId: 'worker-abc',
});
const toolRegistry = new Map([
[AgentConstants.BASH_TOOL, { name: AgentConstants.BASH_TOOL }],
]);
const result = await loadToolsForExecution({
req,
res: {},
agent: {
id: 'attached-agent',
tools: [Tools.execute_code],
stateful_code_sessions: true,
stateful_code_environment: 'agent-user',
},
toolNames: [AgentConstants.BASH_TOOL],
toolRegistry,
actionsEnabled: false,
});
expect(mockCreateAttachedWorkspaceBashTool).toHaveBeenCalledWith({
authHeaders: expect.any(Function),
baseUrl: 'http://attached-code.test/v1',
});
expect(result.loadedTools).toContainEqual({ name: AgentConstants.BASH_TOOL });
});
it('resolves stateful routing when handle_skill is the only requested tool', async () => {
const capabilities = [
AgentCapabilities.tools,

View file

@ -23,8 +23,16 @@ jest.mock('@librechat/agents', () => ({
BashExecutionToolDefinition: {
name: 'bash_tool',
description: 'bash',
schema: { type: 'object', properties: {} },
schema: {
type: 'object',
properties: {
command: { type: 'string' },
args: { type: 'array', items: { type: 'string' } },
},
required: ['command'],
},
},
BashToolOutputReferencesGuide: '{{tool<idx>turn<turn>}}',
/**
* Deterministic stub mirroring the SDK's `buildBashExecutionToolDescription`:
* appends an LLM-facing reference-syntax marker only when
@ -486,6 +494,7 @@ describe('registerCodeExecutionTools', () => {
});
const readFile = result.toolDefinitions.find((definition) => definition.name === 'read_file');
const bashTool = result.toolDefinitions.find((definition) => definition.name === 'bash_tool');
const searchWorkspace = result.toolDefinitions.find(
(definition) => definition.name === 'search_workspace',
);
@ -500,6 +509,17 @@ describe('registerCodeExecutionTools', () => {
max_lines: { type: 'integer', maximum: 500 },
},
});
expect(bashTool?.description).toContain('selected attached environment');
expect(bashTool?.description).toContain('empty directory');
expect(bashTool?.description).toContain('Network access follows the sandbox policy');
expect(bashTool?.description).not.toContain('/mnt/data');
expect(bashTool?.parameters).toMatchObject({
properties: {
command: { type: 'string' },
args: { type: 'array' },
},
required: ['command'],
});
expect(searchWorkspace).toMatchObject({
name: 'search_workspace',
parameters: {

View file

@ -13,6 +13,10 @@ import {
import type { AgentToolOptions, GraphEdge } from 'librechat-data-provider';
import type { LCTool, LCToolRegistry } from '@librechat/agents';
import type { ReachableAgent } from './traversal';
import {
ATTACHED_WORKSPACE_BASH_SCHEMA,
buildAttachedWorkspaceBashDescription,
} from '~/code/command';
import { toolkitExpansion } from '~/tools/toolkits/mapping';
import { normalizeAgentToolKeys } from '~/mcp/utils';
import { collectReachableAgents } from './traversal';
@ -765,15 +769,23 @@ export function isFileAuthoringToolDefinition(def: LCTool | undefined): boolean
* intent of the original constant while keeping the per-agent gate
* behavior introduced for tool-output references.
*/
function createBashToolDef(enableToolOutputReferences: boolean, statefulSessions = false): LCTool {
function createBashToolDef(
enableToolOutputReferences: boolean,
statefulSessions = false,
workspaceTools = false,
): LCTool {
/* Passed as a variable (not an inline literal) so the extra
* `statefulSessions` key stays assignable against pinned SDK versions
* whose builder predates it (ignored at runtime there). */
const descriptionOpts = { enableToolOutputReferences, statefulSessions };
return Object.freeze({
name: BashExecutionToolDefinition.name,
description: buildBashExecutionToolDescription(descriptionOpts),
parameters: BashExecutionToolDefinition.schema as unknown as LCTool['parameters'],
description: workspaceTools
? buildAttachedWorkspaceBashDescription(enableToolOutputReferences)
: buildBashExecutionToolDescription(descriptionOpts),
parameters: (workspaceTools
? ATTACHED_WORKSPACE_BASH_SCHEMA
: BashExecutionToolDefinition.schema) as unknown as LCTool['parameters'],
}) as LCTool;
}
@ -783,11 +795,16 @@ const BASH_TOOL_DEF_WITHOUT_OUTPUT_REFS = createBashToolDef(false);
function buildBashToolDef(opts: {
enableToolOutputReferences: boolean;
statefulSessions?: boolean;
workspaceTools?: boolean;
}): LCTool {
/* Stateful defs are built on demand: the stateless pair covers the
* default path, and per-run construction is negligible next to init. */
if (opts.statefulSessions === true) {
return createBashToolDef(opts.enableToolOutputReferences, true);
if (opts.statefulSessions === true || opts.workspaceTools === true) {
return createBashToolDef(
opts.enableToolOutputReferences,
opts.statefulSessions === true,
opts.workspaceTools === true,
);
}
return opts.enableToolOutputReferences
? BASH_TOOL_DEF_WITH_OUTPUT_REFS
@ -823,7 +840,10 @@ export function registerCodeExecutionTools(
const readFileDef = buildReadFileDef(includeSkillFileInstructions, workspaceTools);
const codeTools: LCTool[] = includeBash
? [readFileDef, buildBashToolDef({ enableToolOutputReferences, statefulSessions })]
? [
readFileDef,
buildBashToolDef({ enableToolOutputReferences, statefulSessions, workspaceTools }),
]
: [readFileDef];
const candidates = workspaceTools
? [...codeTools, SEARCH_WORKSPACE_TOOL_DEF, LIST_WORKSPACE_FILES_TOOL_DEF]

View file

@ -0,0 +1,95 @@
import type { CodeBridgeFetch } from './bridge';
import { createAttachedWorkspaceBashTool } from './command';
function commandResponse(overrides: Record<string, unknown> = {}): Response {
return new Response(
JSON.stringify({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
exitCode: 0,
stdout: 'ready\n',
stderr: '',
truncated: false,
timedOut: false,
...overrides,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } },
);
}
describe('createAttachedWorkspaceBashTool', () => {
test('executes in the attached default workspace with per-user bridge authentication', async () => {
const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse());
const authHeaders = jest.fn().mockResolvedValue({
Authorization: 'Bearer jwt',
'X-LibreChat-Code-Worker-ID': 'user-worker',
});
const bashTool = createAttachedWorkspaceBashTool({
baseUrl: 'https://code.example.com/v1/',
authHeaders,
fetchImpl,
});
await expect(bashTool.func({ command: 'pwd' }, undefined, {})).resolves.toEqual([
'stdout:\nready\n\n[exit code: 0]',
{},
]);
expect(authHeaders).toHaveBeenCalledTimes(1);
expect(fetchImpl).toHaveBeenCalledWith(
'https://code.example.com/v1/workspace-tools/execute',
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer jwt',
'X-LibreChat-Code-Worker-ID': 'user-worker',
}),
}),
);
const request = JSON.parse(String((fetchImpl as jest.Mock).mock.calls[0][1]?.body));
expect(request).toEqual({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
command: 'pwd',
maxOutputBytes: 256 * 1024,
});
});
test('preserves legacy positional args without interpolating shell metacharacters', async () => {
const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse());
const bashTool = createAttachedWorkspaceBashTool({
baseUrl: 'https://code.example.com/v1',
authHeaders: () => ({}),
fetchImpl,
});
await bashTool.func({ command: 'printf "%s" "$1"', args: ["a'b; echo unsafe"] }, undefined, {});
const request = JSON.parse(String((fetchImpl as jest.Mock).mock.calls[0][1]?.body));
expect(request.command).toBe(`bash -c 'printf "%s" "$1"' -- 'a'"'"'b; echo unsafe'`);
});
test('reports termination, timeouts, and truncation without hiding stderr', async () => {
const fetchImpl: CodeBridgeFetch = jest.fn(async () =>
commandResponse({
exitCode: null,
signal: 'SIGKILL',
stdout: '',
stderr: 'deadline reached',
truncated: true,
timedOut: true,
}),
);
const bashTool = createAttachedWorkspaceBashTool({
baseUrl: 'https://code.example.com/v1',
authHeaders: () => ({}),
fetchImpl,
});
await expect(bashTool.func({ command: 'sleep 60' }, undefined, {})).resolves.toEqual([
'stderr:\ndeadline reached\n[terminated by SIGKILL][timed out][output truncated]',
{},
]);
});
});

View file

@ -0,0 +1,111 @@
import { tool } from '@librechat/agents/langchain/tools';
import { BashExecutionToolDefinition, BashToolOutputReferencesGuide } from '@librechat/agents';
import type { DynamicStructuredTool } from '@librechat/agents/langchain/tools';
import type { LCTool } from '@librechat/agents';
import type { WorkspaceExecuteCommandResult } from './workspace';
import type { CodeBridgeFetch } from './bridge';
import { executeWorkspaceTool } from './workspace';
const DEFAULT_WORKSPACE_ID = 'primary';
const DEFAULT_OUTPUT_BYTES = 256 * 1024;
export const ATTACHED_WORKSPACE_BASH_DESCRIPTION = `Runs bash commands inside the selected attached environment and returns stdout/stderr. The workspace may be an existing project, a Git repository, or an empty directory; Git is not required.
Session behavior:
- Files in the registered workspace persist between calls.
- Each call runs in a fresh sandboxed process; shell variables, the working directory, temporary files, and background processes do not survive the call.
- Network access follows the sandbox policy configured on the worker and may be unavailable.
- Commands and file access remain confined by the worker's runtime policy.
- Input code is already displayed to the user; do not repeat it unless asked.
- Explicitly print every result the user should see.
- Never use this tool to execute malicious commands.`;
const bashSchema = BashExecutionToolDefinition.schema as {
properties?: NonNullable<LCTool['parameters']>['properties'];
};
const attachedCommandSchema: NonNullable<LCTool['parameters']> = {
...bashSchema.properties?.command,
type: 'string',
description:
'The bash command or script to execute from the attached workspace root. Files written in the workspace persist between calls, but each call starts a fresh process.',
};
export const ATTACHED_WORKSPACE_BASH_SCHEMA: NonNullable<LCTool['parameters']> = Object.freeze({
type: 'object',
properties: {
...bashSchema.properties,
command: attachedCommandSchema,
},
required: ['command'],
});
export function buildAttachedWorkspaceBashDescription(enableToolOutputReferences: boolean): string {
return enableToolOutputReferences
? `${ATTACHED_WORKSPACE_BASH_DESCRIPTION}\n\n${BashToolOutputReferencesGuide}`
: ATTACHED_WORKSPACE_BASH_DESCRIPTION;
}
function quoteShellArgument(value: string): string {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function commandWithArguments(command: string, args: string[] | undefined): string {
if (!args?.length) return command;
return `bash -c ${quoteShellArgument(command)} -- ${args.map(quoteShellArgument).join(' ')}`;
}
function formatCommandResult(result: WorkspaceExecuteCommandResult): string {
let output = '';
if (result.stdout.length > 0) output += `stdout:\n${result.stdout}\n`;
if (result.stderr.length > 0) output += `stderr:\n${result.stderr}\n`;
if (output.length === 0) output = 'Command completed with no output.\n';
if (result.exitCode != null) output += `[exit code: ${result.exitCode}]`;
if (result.signal != null) output += `[terminated by ${result.signal}]`;
if (result.timedOut) output += '[timed out]';
if (result.truncated) output += '[output truncated]';
return output;
}
export function createAttachedWorkspaceBashTool({
baseUrl,
authHeaders,
workspaceId = DEFAULT_WORKSPACE_ID,
fetchImpl,
}: {
baseUrl: string;
authHeaders: () => Promise<Record<string, string>> | Record<string, string>;
workspaceId?: string;
fetchImpl?: CodeBridgeFetch;
}): DynamicStructuredTool {
return tool(
async (
rawInput: { command: string; args?: string[]; intent?: string },
config,
): Promise<[string, Record<string, never>]> => {
const command = commandWithArguments(rawInput.command, rawInput.args);
const result = await executeWorkspaceTool({
baseURL: baseUrl,
authHeaders: await authHeaders(),
request: {
protocolVersion: 1,
operation: 'execute_command',
workspaceId,
command,
maxOutputBytes: DEFAULT_OUTPUT_BYTES,
},
signal: config?.signal,
fetchImpl,
});
if (result.operation !== 'execute_command') {
throw new Error('Attached workspace returned an unexpected command result.');
}
return [formatCommandResult(result), {}];
},
{
name: BashExecutionToolDefinition.name,
description: ATTACHED_WORKSPACE_BASH_DESCRIPTION,
schema: ATTACHED_WORKSPACE_BASH_SCHEMA,
responseFormat: 'content_and_artifact',
},
) as unknown as DynamicStructuredTool;
}

View file

@ -4,3 +4,4 @@ export * from './config';
export * from './bridge';
export * from './lifecycle';
export * from './workspace';
export * from './command';

View file

@ -560,4 +560,78 @@ describe('executeWorkspaceTool', () => {
).rejects.toMatchObject({ reason: 'invalid' });
expect(fetchImpl).not.toHaveBeenCalled();
});
test('accepts a bounded command result from the selected attached worker', async () => {
const fetchImpl: CodeBridgeFetch = jest.fn(async () =>
Response.json({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
exitCode: 2,
stdout: '',
stderr: 'not found',
truncated: false,
timedOut: false,
}),
);
await expect(
executeWorkspaceTool({
baseURL: 'https://code.example.com/v1',
authHeaders: { Authorization: 'Bearer jwt' },
request: {
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
command: 'test -f package.json',
maxOutputBytes: 256 * 1024,
},
fetchImpl,
}),
).resolves.toMatchObject({ exitCode: 2, stderr: 'not found' });
});
test('rejects command requests and results outside protocol limits', async () => {
const fetchImpl: CodeBridgeFetch = jest.fn(async () =>
Response.json({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
exitCode: 0,
stdout: 'x'.repeat(9),
stderr: '',
truncated: false,
timedOut: false,
}),
);
await expect(
executeWorkspaceTool({
baseURL: 'https://code.example.com/v1',
authHeaders: {},
request: {
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
command: 'printf x',
maxOutputBytes: 8,
},
fetchImpl,
}),
).rejects.toMatchObject({ reason: 'invalid' });
await expect(
executeWorkspaceTool({
baseURL: 'https://code.example.com/v1',
authHeaders: {},
request: {
protocolVersion: 1,
operation: 'execute_command',
workspaceId: 'primary',
command: 'x'.repeat(32 * 1024 + 1),
},
fetchImpl,
}),
).rejects.toMatchObject({ reason: 'invalid' });
});
});

View file

@ -8,6 +8,13 @@ const MAX_READ_LINES = 500;
const MAX_SEARCH_RESULTS = 200;
const MAX_SEARCH_TEXT_LENGTH = 2000;
const MAX_LIST_RESULTS = 500;
const MAX_COMMAND_BYTES = 32 * 1024;
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
const MAX_COMMAND_TIMEOUT_MS = 5 * 60_000;
const DEFAULT_COMMAND_OUTPUT_BYTES = 256 * 1024;
const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024;
const MAX_COMMAND_SIGNAL_LENGTH = 32;
const WORKSPACE_COMMAND_TRANSPORT_GRACE_MS = 5_000;
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
const READ_RESULT_KEYS = new Set([
'protocolVersion',
@ -36,6 +43,17 @@ const LIST_RESULT_KEYS = new Set([
'truncated',
'nextAfterPath',
]);
const COMMAND_RESULT_KEYS = new Set([
'protocolVersion',
'operation',
'workspaceId',
'exitCode',
'signal',
'stdout',
'stderr',
'truncated',
'timedOut',
]);
export interface WorkspaceReadRequest {
protocolVersion: 1;
@ -64,10 +82,21 @@ export interface WorkspaceListRequest {
afterPath?: string;
}
export interface WorkspaceExecuteCommandRequest {
protocolVersion: 1;
operation: 'execute_command';
workspaceId: string;
command: string;
cwd?: string;
timeoutMs?: number;
maxOutputBytes?: number;
}
export type WorkspaceToolRequest =
| WorkspaceReadRequest
| WorkspaceSearchRequest
| WorkspaceListRequest;
| WorkspaceListRequest
| WorkspaceExecuteCommandRequest;
export interface WorkspaceReadResult {
protocolVersion: 1;
@ -98,7 +127,23 @@ export interface WorkspaceListResult {
nextAfterPath?: string;
}
export type WorkspaceToolResult = WorkspaceReadResult | WorkspaceSearchResult | WorkspaceListResult;
export interface WorkspaceExecuteCommandResult {
protocolVersion: 1;
operation: 'execute_command';
workspaceId: string;
exitCode: number | null;
signal?: string;
stdout: string;
stderr: string;
truncated: boolean;
timedOut: boolean;
}
export type WorkspaceToolResult =
| WorkspaceReadResult
| WorkspaceSearchResult
| WorkspaceListResult
| WorkspaceExecuteCommandResult;
export class WorkspaceToolHttpError extends Error {
constructor(
@ -133,6 +178,14 @@ function isPositiveInteger(value: unknown, maximum: number): boolean {
return Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= maximum;
}
function isUtf8StringWithinBytes(value: unknown, maximum: number): value is string {
return (
typeof value === 'string' &&
Buffer.from(value).toString('utf8') === value &&
new TextEncoder().encode(value).byteLength <= maximum
);
}
function hasOnlyKeys(value: Record<string, unknown>, allowed: ReadonlySet<string>): boolean {
return Object.keys(value).every((key) => allowed.has(key));
}
@ -232,6 +285,17 @@ function isValidRequest(request: WorkspaceToolRequest): boolean {
(request.maxResults == null || isPositiveInteger(request.maxResults, MAX_LIST_RESULTS))
);
}
if (request.operation === 'execute_command') {
return (
isUtf8StringWithinBytes(request.command, MAX_COMMAND_BYTES) &&
request.command.trim().length > 0 &&
!request.command.includes('\0') &&
(request.cwd == null || isSafePath(request.cwd)) &&
(request.timeoutMs == null || isPositiveInteger(request.timeoutMs, MAX_COMMAND_TIMEOUT_MS)) &&
(request.maxOutputBytes == null ||
isPositiveInteger(request.maxOutputBytes, MAX_COMMAND_OUTPUT_BYTES))
);
}
if (request.operation !== 'search_text') {
return false;
}
@ -314,6 +378,32 @@ function isValidResult(
? value.paths.length > 0 && value.nextAfterPath === value.paths[value.paths.length - 1]
: value.nextAfterPath == null;
}
if (request.operation === 'execute_command') {
const stdout = typeof value.stdout === 'string' ? value.stdout : null;
const stderr = typeof value.stderr === 'string' ? value.stderr : null;
const outputLimit = request.maxOutputBytes ?? DEFAULT_COMMAND_OUTPUT_BYTES;
return (
hasOnlyKeys(value, COMMAND_RESULT_KEYS) &&
stdout != null &&
stderr != null &&
Buffer.from(stdout).toString('utf8') === stdout &&
Buffer.from(stderr).toString('utf8') === stderr &&
new TextEncoder().encode(stdout).byteLength + new TextEncoder().encode(stderr).byteLength <=
outputLimit &&
(value.exitCode === null ||
(Number.isSafeInteger(value.exitCode) &&
Number(value.exitCode) >= 0 &&
Number(value.exitCode) <= 255)) &&
(value.signal == null ||
(typeof value.signal === 'string' &&
value.signal.length <= MAX_COMMAND_SIGNAL_LENGTH &&
/^SIG[A-Z0-9]+$/.test(value.signal))) &&
typeof value.timedOut === 'boolean' &&
(value.exitCode === null
? value.timedOut === true || value.signal != null
: value.timedOut === false && value.signal == null)
);
}
const maxResults = request.maxResults ?? 50;
return (
hasOnlyKeys(value, SEARCH_RESULT_KEYS) &&
@ -333,6 +423,11 @@ function isValidResult(
);
}
function getWorkspaceToolTimeoutMs(request: WorkspaceToolRequest): number {
if (request.operation !== 'execute_command') return WORKSPACE_TOOL_TIMEOUT_MS;
return (request.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS) + WORKSPACE_COMMAND_TRANSPORT_GRACE_MS;
}
export async function executeWorkspaceTool({
baseURL,
authHeaders,
@ -350,7 +445,7 @@ export async function executeWorkspaceTool({
throw new WorkspaceToolHttpError('invalid');
}
try {
const timeoutSignal = AbortSignal.timeout(WORKSPACE_TOOL_TIMEOUT_MS);
const timeoutSignal = AbortSignal.timeout(getWorkspaceToolTimeoutMs(request));
const requestSignal =
signal != null && typeof AbortSignal.any === 'function'
? AbortSignal.any([signal, timeoutSignal])