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
14 changes: 11 additions & 3 deletions docs/remote-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,24 @@ leave Code API, and are bounded to 1 MiB/500 lines for reads, 200 matches for
searches, or 500 relative paths for file listings. Absolute paths, traversal,
backslashes, symlink escapes, unexpected fields, and host roots are rejected.

Workspace mutation remains disabled unless the operator starts the worker with
`--allow-workspace-writes` (or
`LIBRECHAT_CODE_ALLOW_WORKSPACE_WRITES=true`). That adds bounded `write_file`
and exact-match `edit_file` operations. Writes are limited to 1 MiB of UTF-8
text, require an existing in-workspace parent directory, reject symlinks, and
commit atomically. The worker capability is an enforcement boundary; LibreChat
should still route every mutation through its configurable tool-approval hooks.

The workspace root can be an existing project, a Git repository, or an empty
directory; Git is not required. This boundary keeps that directory local to the
operator's machine, but selected file contents, search matches, relative file
listings, and later tool results necessarily cross the outbound bridge to Code
API and the model.
Treat them as explicit tool outputs, apply the same retention and audit policy
as chat content, and do not register a directory containing secrets. The
default operations are read-only; future mutation and shell operations must be
gated by LibreChat's tool-approval hooks in addition to worker capability
checks.
default operations are read-only. Shell execution remains a separate future
capability because it requires a sandboxed process boundary in addition to
LibreChat's tool-approval hooks and worker capability checks.

Stateful deployments must also set `LIBRECHAT_CODE_STATEFUL_WORKSPACE=true`
and route the CLI's `{runtimeSessionId}` endpoint template to an isolated,
Expand Down
40 changes: 35 additions & 5 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ stateful result remains ambiguous, it exits with a quarantine error instead of
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.
Likewise, if a local `write_file` or `edit_file` completes but its fulfilled
settlement cannot be acknowledged, the worker exits before accepting more
workspace operations and writes a deployment/worker/workspace-scoped
quarantine marker that survives process restarts. The marker is armed before
each mutation with exclusive, incarnation-owned creation and removed only after
Code API accepts its settlement. Overlapping workers cannot replace or clear
one another's marker. The worker refuses to register writable workspace tools
while that marker exists. Inspect or restore the registered directory, then
explicitly clear the marker with
`librechat-code clear-workspace-quarantine --worker-dir <same-directory>`
before restarting it. Use `--default-workspace --workspace-id <id>` instead for
an application-owned default directory. `LIBRECHAT_CODE_WORKSPACE_QUARANTINE_FILE`
may override the marker path for managed deployments.

## Local workspace tools (bridge preview)

Expand All @@ -196,7 +209,10 @@ may be an existing project, a Git repository, or a newly created empty
directory; Git is optional.
`LocalWorkspaceTools` registers opaque workspace IDs with optional display
names and exposes bounded `read_file`, literal `search_text`, and deterministic
`list_files` operations.
`list_files` operations. Workspace mutation is disabled by default. Operators
can explicitly add confined `write_file` and exact-match `edit_file` operations
with `--allow-workspace-writes` or
`LIBRECHAT_CODE_ALLOW_WORKSPACE_WRITES=true`.
Only IDs, names, protocol version, and supported operations appear in worker
capabilities; absolute host paths remain local to the worker process.

Expand All @@ -211,6 +227,15 @@ stop after bounded global result counts. The worker process still belongs inside
the trusted BYOM boundary and should receive filesystem access only to roots the
operator intentionally registers.

Writes are limited to 1 MiB of UTF-8 text and require an existing directory
inside the registered root. They reject traversal, symlink targets, and
non-regular files, and commit through an owner-only temporary file followed by
an atomic rename. The worker syncs the containing directory and verifies that
the installed inode still contains the requested bytes before reporting
success. Edits replace text only when the requested old text occurs exactly
once and reject if the file changes before commit. These operations do not
create directories or execute commands.

Register one directory already present on the worker machine with the
worker-directory option:

Expand All @@ -231,10 +256,9 @@ 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`.
tools are read-only unless writes are explicitly 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
Expand All @@ -244,6 +268,12 @@ explicitly. `rg` must be installed on the worker for `search_text` and
`list_files`. `LIBRECHAT_CODE_DEFAULT_WORKSPACE=true` is the environment
equivalent of `--default-workspace`.

The write flag is an operator capability boundary, not an approval bypass.
LibreChat should allow read, search, and list operations by default and route
write and edit operations through its configurable tool-approval hooks before
dispatch. A worker that was started without write capability rejects mutations
even if a remote caller tries to send one.

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
Expand Down
162 changes: 159 additions & 3 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,56 @@
#!/usr/bin/env node
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { realpath } from 'node:fs/promises';
import { basename, resolve } from 'node:path';

import { pairBridgeWorker } from './pairing.js';
import { startFileRelay } from './relay.js';
import { DockerFileRelaySupervisor } from './relay-runtime.js';
import {
assertWorkspaceMutationQuarantineOwner,
clearWorkspaceMutationQuarantine,
defaultBridgeIdentityPath,
defaultWorkspaceQuarantinePath,
defaultWorkspacePath,
ensurePrivateWorkspaceDirectory,
loadBridgeIdentity,
loadWorkspaceMutationQuarantine,
saveBridgeIdentity,
saveWorkspaceMutationQuarantine,
} 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,
BridgeProtocolError,
isValidBridgeWorkerCapabilities,
isValidBridgeWorkerId,
} from './protocol.js';

function workspaceSecurityIdentity(
pairedPublicKey: string | undefined,
configuredToken: string | undefined,
): string {
return (
pairedPublicKey ?? required('LIBRECHAT_CODE_WORKER_TOKEN', configuredToken)
);
}

function workspaceQuarantinePath(options: {
codeApiUrl: string;
workerId: string;
workspaceRoot?: string;
}): string {
const override = process.env.LIBRECHAT_CODE_WORKSPACE_QUARANTINE_FILE?.trim();
if (override) return override;
return defaultWorkspaceQuarantinePath({
...options,
workspaceRoot: required('workspace directory', options.workspaceRoot),
});
}

function required(name: string, value = process.env[name]): string {
const normalized = value?.trim();
if (!normalized) throw new Error(`${name} is required`);
Expand Down Expand Up @@ -219,6 +248,11 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
(args.includes('--default-workspace') ||
process.env.LIBRECHAT_CODE_DEFAULT_WORKSPACE?.trim().toLowerCase() ===
'true');
const allowWorkspaceWrites =
runtimeSessionId == null &&
(args.includes('--allow-workspace-writes') ||
process.env.LIBRECHAT_CODE_ALLOW_WORKSPACE_WRITES?.trim().toLowerCase() ===
'true');
if (explicitWorkerDirectory && useDefaultWorkspace) {
throw new Error(
'--worker-dir and --default-workspace cannot be used together',
Expand All @@ -229,16 +263,33 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
(useDefaultWorkspace
? defaultWorkspacePath({
codeApiUrl,
securityIdentity:
pairedIdentity?.publicKey ??
required('LIBRECHAT_CODE_WORKER_TOKEN', configuredToken),
securityIdentity: workspaceSecurityIdentity(
pairedIdentity?.publicKey,
configuredToken,
),
workerId,
workspaceId,
})
: undefined);
if (useDefaultWorkspace && workerDirectory) {
await ensurePrivateWorkspaceDirectory(workerDirectory);
}
let canonicalWorkerDirectory: string | undefined;
if (workerDirectory) {
try {
canonicalWorkerDirectory = await realpath(workerDirectory);
} catch {
throw new Error('Invalid workspace registration');
}
}
const mutationQuarantinePath =
allowWorkspaceWrites && canonicalWorkerDirectory
? workspaceQuarantinePath({
codeApiUrl,
workerId,
workspaceRoot: canonicalWorkerDirectory,
})
: undefined;
const workspaceTools = workerDirectory
? await LocalWorkspaceTools.create({
workspaces: [
Expand All @@ -251,6 +302,7 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
? workspaceId
: defaultWorkspaceName(workerDirectory, workspaceId)),
root: workerDirectory,
writable: allowWorkspaceWrites,
},
],
})
Expand Down Expand Up @@ -400,6 +452,44 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
}),
capabilities,
workspaceTools,
workspaceMutationQuarantine: mutationQuarantinePath
? {
async assertAvailable() {
const record = await loadWorkspaceMutationQuarantine(
mutationQuarantinePath,
);
if (record != null) {
throw new BridgeProtocolError(
`Workspace mutations are quarantined since ${record.quarantinedAt}: ${record.reason}. Inspect or restore the workspace, then run librechat-code clear-workspace-quarantine`,
undefined,
'WORKER_QUARANTINED',
);
}
},
async arm(reason) {
await saveWorkspaceMutationQuarantine(mutationQuarantinePath, {
version: 1,
workerId,
workspaceId,
ownerId: incarnationId,
quarantinedAt: new Date().toISOString(),
reason,
});
},
async clear() {
await clearWorkspaceMutationQuarantine(
mutationQuarantinePath,
incarnationId,
);
},
async quarantine() {
await assertWorkspaceMutationQuarantineOwner(
mutationQuarantinePath,
incarnationId,
);
},
}
: undefined,
onIdentityChange:
pairedIdentity && identityPath
? async (identity) => {
Expand Down Expand Up @@ -448,6 +538,68 @@ async function run(runtimeSessionId?: string, args: string[] = []): Promise<void
}
}

async function clearMutationQuarantine(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();
const identityPath =
configuredIdentityPath ??
(configuredWorkerId && !configuredToken
? defaultBridgeIdentityPath(configuredWorkerId)
: undefined);
const pairedIdentity = identityPath
? await loadBridgeIdentity(identityPath)
: undefined;
const workerId = required(
'LIBRECHAT_CODE_WORKER_ID',
configuredWorkerId ?? pairedIdentity?.workerId,
);
const codeApiUrl = required(
'LIBRECHAT_CODE_URL',
process.env.LIBRECHAT_CODE_URL ?? pairedIdentity?.codeApiUrl,
);
const workspaceId =
option(args, '--workspace-id') ??
process.env.LIBRECHAT_CODE_WORKSPACE_ID?.trim() ??
'primary';
const explicitWorkerDirectory = nonEmpty(
option(args, '--worker-dir') ?? process.env.LIBRECHAT_CODE_WORKER_DIR?.trim(),
);
const useDefaultWorkspace =
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: workspaceSecurityIdentity(
pairedIdentity?.publicKey,
configuredToken,
),
workerId,
workspaceId,
})
: undefined);
const path = workspaceQuarantinePath({
codeApiUrl,
workerId,
workspaceRoot: workerDirectory
? await realpath(workerDirectory)
: undefined,
});
await clearWorkspaceMutationQuarantine(path);
process.stdout.write(
`librechat-code: cleared workspace mutation quarantine for ${workspaceId}\n`,
);
}

async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args[0] === 'relay') {
Expand All @@ -468,6 +620,10 @@ async function main(): Promise<void> {
await run(runtimeSessionId);
return;
}
if (args[0] === 'clear-workspace-quarantine') {
await clearMutationQuarantine(args.slice(1));
return;
}
if (args[0] && args[0] !== 'run') {
throw new Error(`Unknown command: ${args[0]}`);
}
Expand Down
Loading