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
21 changes: 20 additions & 1 deletion packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 41 additions & 6 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { startFileRelay } from './relay.js';
import { DockerFileRelaySupervisor } from './relay-runtime.js';
import {
defaultBridgeIdentityPath,
defaultWorkspacePath,
ensurePrivateWorkspaceDirectory,
loadBridgeIdentity,
saveBridgeIdentity,
} from './storage.js';
Expand Down Expand Up @@ -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;
}
Comment on lines +72 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep trimming configured worker directories

When LIBRECHAT_CODE_WORKER_DIR contains incidental leading or trailing whitespace, nonEmpty now verifies the trimmed value but returns the original string, so LocalWorkspaceTools.create calls realpath on a different path and rejects registration. The parent implementation trimmed this environment variable, so this commit regresses previously valid configurations such as LIBRECHAT_CODE_WORKER_DIR=' /srv/project '; return the normalized value for environment input while retaining the intended empty-value handling.

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47d4832. Environment-provided worker directories are trimmed before empty-value normalization, preserving the prior behavior; CLI-provided paths retain their literal spelling. Added a CLI regression test with leading/trailing environment whitespace.


function defaultWorkspaceName(workerDirectory: string, workspaceId: string): string {
const directoryName = basename(resolve(workerDirectory));
return directoryName.trim().length > 0 &&
Expand Down Expand Up @@ -197,15 +203,42 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
runtimeMode === 'docker-macos-nsjail' &&
runtimeSessionId == null &&
(fileRelayUpstream?.length ?? 0) > 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: [
Expand All @@ -214,7 +247,9 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
name:
option(args, '--workspace-name') ??
process.env.LIBRECHAT_CODE_WORKSPACE_NAME?.trim() ??
defaultWorkspaceName(workerDirectory, workspaceId),
(useDefaultWorkspace
? workspaceId
: defaultWorkspaceName(workerDirectory, workspaceId)),
root: workerDirectory,
},
],
Expand Down
61 changes: 61 additions & 0 deletions packages/code/src/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import test from 'node:test';

import {
defaultBridgeIdentityPath,
defaultWorkspacePath,
ensurePrivateWorkspaceDirectory,
loadBridgeIdentity,
saveBridgeIdentity,
} from './storage.js';
Expand All @@ -17,6 +19,65 @@ test('default identity paths do not collide after worker ID sanitization', () =>
);
});

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');
Expand Down
56 changes: 52 additions & 4 deletions packages/code/src/storage.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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),
Comment on lines +68 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Namespace default workspaces by bridge identity

When the same host account re-pairs a worker ID for another tenant, this path remains identical because it includes only the public worker and workspace IDs. RedisBridgePairingStore.redeem creates a new stable identityId and tenant binding on every pairing, but the newly bound worker will advertise the previous identity's persisted files through read_file, search_text, and list_files; the same leak occurs when an ID is reused against another Code API deployment. Include the paired identity/deployment in the storage namespace, or explicitly clear the workspace when the security identity changes.

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 38d0d50. Default paths now include a deployment/security-identity namespace derived from the normalized Code API URL plus the paired public key (or static credential), ahead of the worker/workspace digests. Credential refresh within one pairing remains stable; re-pairing or changing deployments produces an isolated path. Tests cover both identity and deployment changes.

);
}

export async function ensurePrivateWorkspaceDirectory(
path: string,
): Promise<void> {
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,
Expand Down
68 changes: 67 additions & 1 deletion packages/code/src/workspace-cli.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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, ' ');
Expand Down Expand Up @@ -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 });
}
});