From af1b6e5b706943cbf158b1c17e4229884f5de3af Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 2 Sep 2026 14:29:27 -0400 Subject: [PATCH] feat(code): create an explicit default workspace --- packages/code/README.md | 21 +++++++- packages/code/src/cli.ts | 47 ++++++++++++++--- packages/code/src/storage.test.ts | 61 ++++++++++++++++++++++ packages/code/src/storage.ts | 56 ++++++++++++++++++-- packages/code/src/workspace-cli.test.ts | 68 ++++++++++++++++++++++++- 5 files changed, 241 insertions(+), 12 deletions(-) diff --git a/packages/code/README.md b/packages/code/README.md index be421760..f5483cf8 100644 --- a/packages/code/README.md +++ b/packages/code/README.md @@ -218,12 +218,31 @@ worker-directory option: librechat-code run --worker-dir /path/to/workspace ``` +To start without an existing project or Git repository, explicitly ask the +worker to create and reuse an application-owned workspace: + +```bash +librechat-code run --default-workspace +``` + +The directory is created with owner-only permissions below +`~/.local/share/librechat/code/workspaces/`, using stable digests of the worker +and workspace IDs so distinct IDs cannot alias on case-insensitive filesystems. +The deployment and paired bridge identity are also part of the namespace, so +re-pairing or switching Code API deployments cannot expose the previous +identity's files. It persists across worker restarts. The current workspace +tools are read-only, so an empty directory must be populated by a local process +until write-capable coding tools are enabled. The worker never registers its +process working directory implicitly, and `--default-workspace` cannot be +combined with `--worker-dir`. + The default public workspace ID is `primary` and the default display name is the directory basename. Operators can use `--workspace-id` and `--workspace-name`, or `LIBRECHAT_CODE_WORKER_DIR`, `LIBRECHAT_CODE_WORKSPACE_ID`, and `LIBRECHAT_CODE_WORKSPACE_NAME`, to set them explicitly. `rg` must be installed on the worker for `search_text` and -`list_files`. +`list_files`. `LIBRECHAT_CODE_DEFAULT_WORKSPACE=true` is the environment +equivalent of `--default-workspace`. The worker advertises these capabilities only when a directory is configured and executes matching assignments under the bridge's existing lease, diff --git a/packages/code/src/cli.ts b/packages/code/src/cli.ts index a8cb1f93..3f68a9d3 100644 --- a/packages/code/src/cli.ts +++ b/packages/code/src/cli.ts @@ -8,6 +8,8 @@ import { startFileRelay } from './relay.js'; import { DockerFileRelaySupervisor } from './relay-runtime.js'; import { defaultBridgeIdentityPath, + defaultWorkspacePath, + ensurePrivateWorkspaceDirectory, loadBridgeIdentity, saveBridgeIdentity, } from './storage.js'; @@ -67,6 +69,10 @@ function option(args: string[], name: string): string | undefined { return args.find((value) => value.startsWith(`${name}=`))?.slice(name.length + 1); } +function nonEmpty(value: string | undefined): string | undefined { + return value?.trim().length ? value : undefined; +} + function defaultWorkspaceName(workerDirectory: string, workspaceId: string): string { const directoryName = basename(resolve(workerDirectory)); return directoryName.trim().length > 0 && @@ -197,15 +203,42 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise 0; - const workerDirectory = - runtimeSessionId == null - ? option(args, '--worker-dir') ?? - process.env.LIBRECHAT_CODE_WORKER_DIR?.trim() - : undefined; const workspaceId = option(args, '--workspace-id') ?? process.env.LIBRECHAT_CODE_WORKSPACE_ID?.trim() ?? 'primary'; + const explicitWorkerDirectory = + runtimeSessionId == null + ? nonEmpty( + option(args, '--worker-dir') ?? + process.env.LIBRECHAT_CODE_WORKER_DIR?.trim(), + ) + : undefined; + const useDefaultWorkspace = + runtimeSessionId == null && + (args.includes('--default-workspace') || + process.env.LIBRECHAT_CODE_DEFAULT_WORKSPACE?.trim().toLowerCase() === + 'true'); + if (explicitWorkerDirectory && useDefaultWorkspace) { + throw new Error( + '--worker-dir and --default-workspace cannot be used together', + ); + } + const workerDirectory = + explicitWorkerDirectory ?? + (useDefaultWorkspace + ? defaultWorkspacePath({ + codeApiUrl, + securityIdentity: + pairedIdentity?.publicKey ?? + required('LIBRECHAT_CODE_WORKER_TOKEN', configuredToken), + workerId, + workspaceId, + }) + : undefined); + if (useDefaultWorkspace && workerDirectory) { + await ensurePrivateWorkspaceDirectory(workerDirectory); + } const workspaceTools = workerDirectory ? await LocalWorkspaceTools.create({ workspaces: [ @@ -214,7 +247,9 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise ); }); +test('default workspace paths are stable and collision resistant', () => { + const home = '/home/tester'; + const options = { + codeApiUrl: 'https://code.example/v1', + securityIdentity: 'bridge-public-key', + workerId: 'vm-1', + workspaceId: 'primary', + homeDirectory: home, + }; + assert.equal( + defaultWorkspacePath(options), + defaultWorkspacePath({ ...options, codeApiUrl: 'https://code.example/v1/' }), + ); + assert.notEqual( + defaultWorkspacePath({ ...options, workerId: 'vm:a' }), + defaultWorkspacePath({ ...options, workerId: 'vm_a' }), + ); + assert.notEqual( + defaultWorkspacePath({ ...options, workerId: 'vm:a' }), + defaultWorkspacePath({ + ...options, + workerId: 'vm_a-2d4fcea9e21e004d', + }), + ); + assert.notEqual( + defaultWorkspacePath({ ...options, workerId: 'VM-1' }).toLowerCase(), + defaultWorkspacePath({ ...options, workerId: 'vm-1' }).toLowerCase(), + ); + assert.notEqual( + defaultWorkspacePath(options), + defaultWorkspacePath({ + ...options, + securityIdentity: 'new-pairing-public-key', + }), + ); + assert.notEqual( + defaultWorkspacePath(options), + defaultWorkspacePath({ + ...options, + codeApiUrl: 'https://other-code.example/v1', + }), + ); +}); + +test('default workspace directories are created with owner-only permissions', async () => { + const directory = await mkdtemp( + join(tmpdir(), 'librechat-code-workspace-home-'), + ); + const path = join(directory, 'workspaces', 'primary'); + try { + await ensurePrivateWorkspaceDirectory(path); + const metadata = await stat(path); + assert.equal(metadata.isDirectory(), true); + assert.equal(metadata.mode & 0o777, 0o700); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + test('paired identity is persisted atomically with owner-only permissions', async () => { const directory = await mkdtemp(join(tmpdir(), 'librechat-code-')); const path = join(directory, 'identity.json'); diff --git a/packages/code/src/storage.ts b/packages/code/src/storage.ts index a26c3eb0..adeab1bd 100644 --- a/packages/code/src/storage.ts +++ b/packages/code/src/storage.ts @@ -1,5 +1,5 @@ import { createHash, randomBytes } from 'node:crypto'; -import { chmod, mkdir, open, readFile, rename, rm } from 'node:fs/promises'; +import { chmod, lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises'; import { homedir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -27,12 +27,60 @@ function isPairedIdentity(value: unknown): value is PairedBridgeWorkerIdentity { export function defaultBridgeIdentityPath(workerId: string): string { const readableName = workerId.replace(/[^A-Za-z0-9._-]/g, '_'); - const fileName = readableName === workerId - ? readableName - : `${readableName}-${createHash('sha256').update(workerId).digest('hex').slice(0, 16)}`; + const fileName = + readableName === workerId + ? readableName + : `${readableName}-${createHash('sha256') + .update(workerId) + .digest('hex') + .slice(0, 16)}`; return join(homedir(), '.config', 'librechat', 'code', `${fileName}.json`); } +function workspaceStorageName(value: string): string { + return `id-${createHash('sha256').update(value).digest('hex')}`; +} + +export interface DefaultWorkspacePathOptions { + codeApiUrl: string; + securityIdentity: string; + workerId: string; + workspaceId: string; + homeDirectory?: string; +} + +export function defaultWorkspacePath({ + codeApiUrl, + securityIdentity, + workerId, + workspaceId, + homeDirectory = homedir(), +}: DefaultWorkspacePathOptions): string { + const deploymentIdentity = `${codeApiUrl.replace(/\/+$/, '')}\0${securityIdentity}`; + return join( + homeDirectory, + '.local', + 'share', + 'librechat', + 'code', + 'workspaces', + workspaceStorageName(deploymentIdentity), + workspaceStorageName(workerId), + workspaceStorageName(workspaceId), + ); +} + +export async function ensurePrivateWorkspaceDirectory( + path: string, +): Promise { + await mkdir(path, { recursive: true, mode: 0o700 }); + const metadata = await lstat(path); + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new BridgeProtocolError('Default workspace path must be a directory'); + } + await chmod(path, 0o700); +} + export async function saveBridgeIdentity( path: string, identity: PairedBridgeWorkerIdentity, diff --git a/packages/code/src/workspace-cli.test.ts b/packages/code/src/workspace-cli.test.ts index 4ac486ee..67692cab 100644 --- a/packages/code/src/workspace-cli.test.ts +++ b/packages/code/src/workspace-cli.test.ts @@ -1,13 +1,15 @@ import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { once } from 'node:events'; -import { mkdtemp, mkdir, rm } from 'node:fs/promises'; +import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises'; import { createServer } from 'node:http'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; +import { defaultWorkspacePath } from './storage.js'; + test('CLI validates a configured worker directory before registration', () => { const result = spawnSync( process.execPath, @@ -32,6 +34,30 @@ test('CLI validates a configured worker directory before registration', () => { assert.match(result.stderr, /invalid workspace registration/i); }); +test('CLI trims an environment-configured worker directory', async (t) => { + const workspaceRoot = await mkdtemp( + join(tmpdir(), 'librechat-code-env-workspace-'), + ); + t.after(() => rm(workspaceRoot, { recursive: true, force: true })); + const result = spawnSync( + process.execPath, + [fileURLToPath(new URL('./cli.js', import.meta.url)), 'run'], + { + encoding: 'utf8', + timeout: 500, + env: { + ...process.env, + LIBRECHAT_CODE_URL: 'http://127.0.0.1:1/v1', + LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', + LIBRECHAT_CODE_WORKER_ID: 'engineering-vm', + LIBRECHAT_CODE_WORKER_DIR: ` ${workspaceRoot} `, + }, + }, + ); + + assert.doesNotMatch(result.stderr, /invalid workspace registration/i); +}); + test('CLI falls back to the workspace ID when the directory basename is invalid', async (t) => { const root = await mkdtemp(join(tmpdir(), 'librechat-code-cli-')); const workspaceRoot = join(root, ' '); @@ -116,3 +142,43 @@ test('CLI falls back to the workspace ID when the directory basename is invalid' }, ); }); + +test('CLI explicitly creates and registers an application-owned default workspace', async () => { + const testHome = await mkdtemp(join(tmpdir(), 'librechat-code-home-')); + try { + const result = spawnSync( + process.execPath, + [ + fileURLToPath(new URL('./cli.js', import.meta.url)), + 'run', + '--default-workspace', + ], + { + encoding: 'utf8', + timeout: 500, + env: { + ...process.env, + HOME: testHome, + LIBRECHAT_CODE_URL: 'http://127.0.0.1:1/v1', + LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret', + LIBRECHAT_CODE_WORKER_ID: 'engineering-vm', + LIBRECHAT_CODE_WORKER_DIR: ' ', + }, + }, + ); + + assert.doesNotMatch(result.stderr, /invalid workspace registration/i); + const workspace = defaultWorkspacePath({ + codeApiUrl: 'http://127.0.0.1:1/v1', + securityIdentity: 'worker-secret', + workerId: 'engineering-vm', + workspaceId: 'primary', + homeDirectory: testHome, + }); + const metadata = await stat(workspace); + assert.equal(metadata.isDirectory(), true); + assert.equal(metadata.mode & 0o777, 0o700); + } finally { + await rm(testHome, { recursive: true, force: true }); + } +});