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
27 changes: 23 additions & 4 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ accepting another assignment. Reset or discard that session's local runner
before restarting the worker; its workspace may contain mutations that Code
API did not commit.

## Local workspace tools (library preview)
## Local workspace tools (bridge preview)

`@librechat/code/workspace` provides the provider-neutral foundation for
coding-agent access to repositories that already live on the worker machine.
Expand All @@ -207,9 +207,28 @@ and stops after a bounded global result count. The worker process still belongs
inside the trusted BYOM boundary and should receive filesystem access only to
roots the operator intentionally registers.

This release exposes the library protocol only. The subsequent bridge-dispatch
layer will route signed, deadline-bound workspace tool assignments to it; until
that layer is configured, the CLI does not advertise or execute these tools.
Register one repository already present on the worker machine with the
Cursor-style worker-directory option:

```bash
librechat-code run --worker-dir /path/to/repository
```

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`.

The worker advertises these capabilities only when a directory is configured
and executes matching assignments under the bridge's existing lease,
deadline, cancellation, credential-refresh, and settlement fencing. The
repository itself remains on the worker. As with Cursor's self-hosted agents,
text deliberately selected by `read_file` or `search_text` crosses the outbound
bridge so the remote agent/model can reason over it. Host paths are never part
of that payload. The Code API workspace-tool endpoint is delivered as a
dependent layer; deployments without it continue to use sandbox assignments
unchanged.

After discarding or resetting that session's local runner, acknowledge recovery
with `librechat-code reset-workspace <runtime-session-id>`. The command uses the
Expand Down
41 changes: 38 additions & 3 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { basename, resolve } from 'node:path';

import { pairBridgeWorker } from './pairing.js';
import { startFileRelay } from './relay.js';
Expand All @@ -13,7 +13,9 @@ import {
} from './storage.js';
import { BridgeWorker } from './worker.js';
import { DockerRuntimeSupervisor, EndpointRuntimeSupervisor } from './runtime.js';
import { LocalWorkspaceTools } from './workspace.js';
import {
BRIDGE_WORKSPACE_NAME_MAX_LENGTH,
isValidBridgeWorkerCapabilities,
isValidBridgeWorkerId,
} from './protocol.js';
Expand Down Expand Up @@ -65,6 +67,14 @@ function option(args: string[], name: string): string | undefined {
return args.find((value) => value.startsWith(`${name}=`))?.slice(name.length + 1);
}

function defaultWorkspaceName(workerDirectory: string, workspaceId: string): string {
const directoryName = basename(resolve(workerDirectory));
return directoryName.trim().length > 0 &&
directoryName.length <= BRIDGE_WORKSPACE_NAME_MAX_LENGTH
? directoryName
: workspaceId;
}

async function pair(args: string[]): Promise<void> {
const codeApiUrl = required('instance URL', args[1]);
const code = required('one-time pairing code', args[2]);
Expand Down Expand Up @@ -117,7 +127,7 @@ async function relay(): Promise<void> {
await handle.close();
}

async function run(runtimeSessionId?: string): Promise<void> {
async function run(runtimeSessionId?: string, args: string[] = []): Promise<void> {
const configuredWorkerId = process.env.LIBRECHAT_CODE_WORKER_ID?.trim();
const configuredIdentityPath = process.env.LIBRECHAT_CODE_IDENTITY_FILE?.trim();
const configuredToken = process.env.LIBRECHAT_CODE_WORKER_TOKEN?.trim();
Expand Down Expand Up @@ -187,6 +197,29 @@ async function run(runtimeSessionId?: 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 workspaceTools = workerDirectory
? await LocalWorkspaceTools.create({
workspaces: [
{
id: workspaceId,
name:
option(args, '--workspace-name') ??
process.env.LIBRECHAT_CODE_WORKSPACE_NAME?.trim() ??
defaultWorkspaceName(workerDirectory, workspaceId),
root: workerDirectory,
},
],

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 Fall back from invalid directory basenames

When --worker-dir points to a valid directory whose basename is whitespace-only or longer than BRIDGE_WORKSPACE_NAME_MAX_LENGTH (for example, a 129-character Linux directory name), this default is passed as the workspace name and LocalWorkspaceTools.create rejects the registration. In this head the fallback only handles an empty basename, so these directories still require an otherwise unnecessary explicit --workspace-name; validate the derived basename and fall back to the workspace ID when it cannot be advertised.

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 4ccea0df. Automatically derived workspace names are now accepted only when non-whitespace and within the protocol length limit; otherwise the CLI advertises the configured workspace ID. Explicit invalid names still fail validation. The package suite passes all 142 tests, including a real whitespace-named directory registration regression.

})
: undefined;
const capabilities = {
statefulWorkspace,
sandboxProfile:
Expand All @@ -195,6 +228,7 @@ async function run(runtimeSessionId?: string): Promise<void> {
runtimes: list(process.env.LIBRECHAT_CODE_RUNTIMES),
policyDigest: createHash('sha256').update(policy).digest('hex'),
...(fileRelayEnabled ? { requiresReadyConfirmation: true } : {}),
...(workspaceTools ? { workspaceTools: workspaceTools.capabilities } : {}),
};
if (!isValidBridgeWorkerCapabilities(capabilities)) {
throw new Error(
Expand Down Expand Up @@ -330,6 +364,7 @@ async function run(runtimeSessionId?: string): Promise<void> {
statefulWorkspace,
}),
capabilities,
workspaceTools,
onIdentityChange:
pairedIdentity && identityPath
? async (identity) => {
Expand Down Expand Up @@ -401,7 +436,7 @@ async function main(): Promise<void> {
if (args[0] && args[0] !== 'run') {
throw new Error(`Unknown command: ${args[0]}`);
}
await run();
await run(undefined, args.slice(1));
}
main().catch((error: Error) => {
process.stderr.write(`librechat-code: ${error.message}\n`);
Expand Down
5 changes: 4 additions & 1 deletion packages/code/src/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { WorkspaceToolRequest } from './workspace.js';

export const BRIDGE_PROTOCOL_VERSION = 1 as const;
export const BRIDGE_WORKER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export const BRIDGE_SANDBOX_PROFILE_MAX_LENGTH = 128;
Expand Down Expand Up @@ -77,7 +79,8 @@ export interface BridgeAssignment<TBody = object> {
/** Server-calculated execution budget at lease time; avoids VM clock skew. */
remainingMs?: number;
runtimeSessionId?: string;
request: BridgeSandboxRequest<TBody>;
executionKind?: 'sandbox' | 'workspace_tool';
request: BridgeSandboxRequest<TBody> | WorkspaceToolRequest;
}

export interface BridgeLeaseResponse<TBody = object> {
Expand Down
Loading