diff --git a/CONTEXT.md b/CONTEXT.md index db384f8b3e..fbc2f1085c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,5 +1,6 @@ # Domain language +- **Attached code environment**: A principal- or deployment-authorized stateful workspace owned by an outbound `librechat-code` worker on a user-chosen machine or VM. LibreChat selects it and enforces approval policy, Code API authenticates and dispatches to it, and the worker's local sandbox and capability flags remain the final execution ceiling. The environment interface is runtime-neutral: native SRT, WSL2, Docker/NsJail, and future adapters expose the same workspace operations without leaking host paths or runtime configuration into agent tools. - **Agent run envelope**: the versioned, JSON-safe request contract created after ingress authentication and protocol validation but before agent, provider, tool, or MCP initialization. It carries only the validated protocol payload and the minimum trusted principal identifiers. The execution host rehydrates all runtime state from those identifiers. - **Agent execution context**: runtime-only, transport-free state rehydrated beside an Agent run envelope. It contains the authenticated user, application configuration, normalized request metadata, and resolved conversation facts needed by initialization, but never Express request/response objects or serialized credentials. - **Agent execution host**: the protocol-neutral module that owns run admission, disconnect cancellation, provider-start fencing, and terminal settlement. Protocol implementations execute behind its callback interface; HTTP adapters retain validation and final stream rendering. diff --git a/client/src/components/Nav/Settings/CodeEnvironments.tsx b/client/src/components/Nav/Settings/CodeEnvironments.tsx index 978ad88a8e..fcc3feb9a9 100644 --- a/client/src/components/Nav/Settings/CodeEnvironments.tsx +++ b/client/src/components/Nav/Settings/CodeEnvironments.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { createCodeWorkerSetupCommand, isCodeWorkerShell } from 'librechat-data-provider'; import { Button, Input, @@ -17,6 +18,7 @@ import { import type { CodeEnvironmentPermissionDecision, TCodeEnvironmentSummary, + CodeWorkerShell, } from 'librechat-data-provider'; import type { CodeEnvironmentPairingResponse } from '~/data-provider/CodeEnvironments'; import { @@ -30,13 +32,39 @@ import { useLocalize } from '~/hooks'; function WorkerCommand({ pairing }: { pairing: CodeEnvironmentPairingResponse['pairing'] }) { const { showToast } = useToastContext(); const localize = useLocalize(); - const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`; - const command = `librechat-code pair ${shellQuote(pairing.endpoint)} ${shellQuote(pairing.code)} --worker-id ${shellQuote(pairing.workerId)}`; + const [shell, setShell] = useState('posix'); + const command = createCodeWorkerSetupCommand(pairing, shell, { + allowWorkspaceWrites: true, + allowWorkspaceCommands: true, + }); return (
-

- {localize('com_ui_code_environment_pairing_ready')} -

+
+

+ {localize('com_ui_code_environment_pairing_ready')} +

+ +

{localize('com_ui_code_environment_pairing_description')}

diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 700ca27855..6323b2ed3f 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -2194,7 +2194,10 @@ "com_ui_code_environment_unavailable": "This LibreChat instance has not enabled self-service code workers.", "com_ui_code_environment_load_error": "Code environments could not be loaded. Try again shortly.", "com_ui_code_environment_pairing_ready": "One-time worker command", - "com_ui_code_environment_pairing_description": "Run this on your VM, then start librechat-code with LIBRECHAT_CODE_SANDBOX_ENDPOINT set to your sandbox service. The command expires shortly and is shown only once.", + "com_ui_code_environment_pairing_description": "Run these commands on your machine to pair the worker and start a persistent default workspace with the native sandbox. The pairing code expires shortly and is shown only once.", + "com_ui_code_environment_shell": "Command shell", + "com_ui_code_environment_shell_posix": "macOS / Linux / WSL2", + "com_ui_code_environment_shell_powershell": "Windows PowerShell", "com_ui_code_environment_pairing_created": "Code environment created", "com_ui_code_environment_pairing_error": "Could not create the code environment", "com_ui_code_environment_remove_title": "Remove code environment?", diff --git a/packages/data-provider/src/code/worker.spec.ts b/packages/data-provider/src/code/worker.spec.ts new file mode 100644 index 0000000000..9500b0c365 --- /dev/null +++ b/packages/data-provider/src/code/worker.spec.ts @@ -0,0 +1,53 @@ +import { createCodeWorkerSetupCommand, isCodeWorkerShell } from './worker'; + +const pairing = { + endpoint: 'https://code.example.com/v1', + code: "one-time'code", + workerId: "worker'one", + expiresAt: '2026-09-04T15:00:00.000Z', +}; + +describe('createCodeWorkerSetupCommand', () => { + it('creates a POSIX and WSL2 command for the native default workspace', () => { + expect( + createCodeWorkerSetupCommand(pairing, 'posix', { + allowWorkspaceWrites: true, + allowWorkspaceCommands: true, + }), + ).toBe( + "librechat-code pair 'https://code.example.com/v1' 'one-time'\\''code' --worker-id 'worker'\\''one'\n" + + "LIBRECHAT_CODE_WORKER_ID='worker'\\''one' librechat-code run --default-workspace --allow-workspace-writes --allow-workspace-commands", + ); + }); + + it('creates a PowerShell command without POSIX quoting', () => { + expect( + createCodeWorkerSetupCommand(pairing, 'powershell', { + allowWorkspaceWrites: true, + allowWorkspaceCommands: true, + }), + ).toBe( + "librechat-code pair 'https://code.example.com/v1' 'one-time''code' --worker-id 'worker''one'\n" + + "$env:LIBRECHAT_CODE_WORKER_ID = 'worker''one'\n" + + 'librechat-code run --default-workspace --allow-workspace-writes --allow-workspace-commands', + ); + }); + + it('defaults to a read-only default workspace command', () => { + expect(createCodeWorkerSetupCommand(pairing, 'posix')).toContain( + 'librechat-code run --default-workspace', + ); + expect(createCodeWorkerSetupCommand(pairing, 'posix')).not.toContain( + '--allow-workspace-writes', + ); + expect(createCodeWorkerSetupCommand(pairing, 'posix')).not.toContain( + '--allow-workspace-commands', + ); + }); + + it('validates shell values at runtime', () => { + expect(isCodeWorkerShell('posix')).toBe(true); + expect(isCodeWorkerShell('powershell')).toBe(true); + expect(isCodeWorkerShell('cmd')).toBe(false); + }); +}); diff --git a/packages/data-provider/src/code/worker.ts b/packages/data-provider/src/code/worker.ts new file mode 100644 index 0000000000..5f7d8861c8 --- /dev/null +++ b/packages/data-provider/src/code/worker.ts @@ -0,0 +1,43 @@ +import type { TCodeEnvironmentPairingResponse } from '../types'; + +export type CodeWorkerShell = 'posix' | 'powershell'; +export type CodeWorkerPairing = TCodeEnvironmentPairingResponse['pairing']; + +export interface CodeWorkerRunOptions { + defaultWorkspace?: boolean; + allowWorkspaceWrites?: boolean; + allowWorkspaceCommands?: boolean; +} + +export function isCodeWorkerShell(value: string): value is CodeWorkerShell { + return value === 'posix' || value === 'powershell'; +} + +function quotePosix(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function quotePowerShell(value: string): string { + return `'${value.replace(/'/g, "''")}'`; +} + +export function createCodeWorkerSetupCommand( + pairing: CodeWorkerPairing, + shell: CodeWorkerShell, + options: CodeWorkerRunOptions = {}, +): string { + const quote = shell === 'powershell' ? quotePowerShell : quotePosix; + const pair = `librechat-code pair ${quote(pairing.endpoint)} ${quote(pairing.code)} --worker-id ${quote(pairing.workerId)}`; + const runArgs = [ + options.defaultWorkspace === false ? null : '--default-workspace', + options.allowWorkspaceWrites === true ? '--allow-workspace-writes' : null, + options.allowWorkspaceCommands === true ? '--allow-workspace-commands' : null, + ].filter((value): value is string => value != null); + const run = ['librechat-code run', ...runArgs].join(' '); + + if (shell === 'powershell') { + return `${pair}\n$env:LIBRECHAT_CODE_WORKER_ID = ${quote(pairing.workerId)}\n${run}`; + } + + return `${pair}\nLIBRECHAT_CODE_WORKER_ID=${quote(pairing.workerId)} ${run}`; +} diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index 8d2030b436..2da796ae7d 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -71,3 +71,4 @@ export * from './parameterSettings'; export * from './agentToolOptions'; /* code-execution sandbox */ export * from './codeEnvRef'; +export * from './code/worker';