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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ jobs:
node-version: 24.16.0
- run: npm ci
- run: npm run build
- name: Selected project root containment tests
run: |
command -v rg || brew install ripgrep
node --test dist/root-access.test.js
node --test --test-name-pattern='selected command|selected replay copy|programmatic probes reject' dist/native-sandbox.test.js
- name: Native environment containment tests
run: node --test dist/environment.test.js
- name: Native ACL and credential lifecycle tests
Expand Down
44 changes: 44 additions & 0 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,50 @@ chosen non-overlapping project directories with `--workspace` or `--environment`
Do not also register their parent directory. Treat the inventory as a snapshot;
normal workspace admission must validate any directory selected from it.

## Register selected projects

After pairing, use paths from `projects --root` to register individual checkouts:

```bash
librechat-code run --project-root /srv/projects \
--project web --project services/api \
--allow-workspace-writes --allow-workspace-commands
```

Only the explicitly listed checkouts become execution roots. The discovery
directory is not registered, and adding a new sibling repository does not grant
access to it. In LibreChat, select the project in the existing workspace picker;
the conversation stores that selection for subsequent tools and approval resumes.
An agent's default workspace and the user's recent selection work as before.

Project IDs are derived from the canonical discovery directory and relative
project path, not the branch or selection order. Keep both paths unchanged across
restarts to retain chat bindings. Moving a checkout changes its ID. These are
registration IDs, not the root-local IDs printed by the inventory command.

Up to 32 selected projects are supported. Each must be a standalone Git checkout;
linked worktrees, symlink traversal, overlapping roots, and duplicate selections
are rejected. Existing native sandbox, command/write permissions, lease-slot and
quarantine rules still apply. This mode cannot be combined with `--environment`,
`--worker-dir`, `--workspace`, default-workspace, or workspace ID/name settings.
Existing registrations are not migrated automatically; use a new conversation
when switching registration mode. Non-Git directories still use the existing
workspace flags. Named environment setup/actions still use `--environment`.

Selected projects require macOS or Linux (including WSL2). Each request opens
and verifies the admitted directory, then retains that descriptor through file
access, repository-instruction loading, command startup, and replay copying.
Renaming a project cannot redirect an in-flight request to a replacement checkout;
subsequent requests reject the changed identity. Restart with an explicitly
selected replacement to admit it. Descriptors close when requests settle, and
independent workspaces do not share a current directory or global execution lock.

This reuses the existing workspace protocol. Programmatic tool calling requires
a LibreChat version that preserves the selected workspace across initial
execution and replay, plus the worker's normal programmatic prerequisites.
Installation alone does not restart workers or change registration; update your
worker service arguments explicitly.

## Pair

Hardened deployments use a one-time code instead of copying a long-lived
Expand Down
34 changes: 33 additions & 1 deletion packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { basename, resolve, relative, isAbsolute, sep } from 'node:path';

import { pairBridgeWorker } from './pairing.js';
import { discoverProjects } from './projects.js';
import { loadProjectRoots, projectRootArguments } from './project-roots.js';
import {
loadCodeEnvironment,
assertEnvironmentDefinitionsOutsideRoots,
Expand Down Expand Up @@ -309,6 +310,26 @@ async function run(
runtimeSessionId?: string,
args: string[] = [],
): Promise<void> {
const projectArgs = projectRootArguments(args);
if (
projectArgs &&
(runtimeSessionId != null ||
args.some(arg =>
['--environment', '--worker-dir', '--default-workspace',
'--workspace', '--workspace-id', '--workspace-name'].some(
flag => arg === flag || arg.startsWith(`${flag}=`),
),
) ||
[process.env.LIBRECHAT_CODE_WORKER_DIR,
process.env.LIBRECHAT_CODE_WORKSPACE_ID,
process.env.LIBRECHAT_CODE_WORKSPACE_NAME].some(value => value?.trim()) ||
process.env.LIBRECHAT_CODE_DEFAULT_WORKSPACE?.trim().toLowerCase() === 'true')
) {
throw new Error('Project selection cannot be combined with other workspace registration settings');
}
const projectRoots = projectArgs
? await loadProjectRoots(projectArgs.root, projectArgs.projects)
: [];
const environmentPaths: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--environment') {
Expand Down Expand Up @@ -426,11 +447,13 @@ async function run(
runtimeSessionId == null &&
(fileRelayUpstream?.length ?? 0) > 0;
const workspaceId =
projectRoots[0]?.id ??
environments[0]?.definition.name ??
option(args, '--workspace-id') ??
process.env.LIBRECHAT_CODE_WORKSPACE_ID?.trim() ??
'primary';
const explicitWorkerDirectory =
projectRoots[0]?.root ??
environments[0]?.definition.root ??
(runtimeSessionId == null
? nonEmpty(
Expand Down Expand Up @@ -468,6 +491,9 @@ async function run(
if (environments.length && commandSandboxMode !== 'native-srt') {
throw new Error('Environment definitions require native-srt');
}
if (projectRoots.length && commandSandboxMode !== 'native-srt') {
throw new Error('Project selections require native-srt');
}
if (
environments.some(environment => environment.definition.setup) &&
!allowWorkspaceCommands
Expand Down Expand Up @@ -575,8 +601,10 @@ async function run(
{
id: workspaceId,
root: canonicalWorkerDirectory,
identity: projectRoots[0]?.identity,
writable: allowWorkspaceWrites,
name:
projectRoots[0]?.name ??
environments[0]?.definition.name ??
option(args, '--workspace-name') ??
process.env.LIBRECHAT_CODE_WORKSPACE_NAME?.trim() ??
Expand All @@ -597,6 +625,9 @@ async function run(
writable: allowWorkspaceWrites,
});
}
for (const project of projectRoots.slice(1)) {
roots.push({ ...project, writable: allowWorkspaceWrites });
}
await assertEnvironmentDefinitionsOutsideRoots(environments, roots);
for (let i = 0; i < args.length; i++) {
if (
Expand Down Expand Up @@ -882,6 +913,7 @@ async function run(
});
const nativeOptions: NativeProcessSandboxOptions = {
workspaceRoot: canonicalWorkerDirectory!,
workspaceIdentity: roots[0]?.identity,
commandPolicy,
protectedPaths: [
identityPath,
Expand Down Expand Up @@ -921,7 +953,7 @@ async function run(
new Map(
roots.map(root => [
root.id,
{ ...nativeOptions, workspaceRoot: root.root },
{ ...nativeOptions, workspaceRoot: root.root, workspaceIdentity: root.identity },
]),
),
workspaceLeaseSlots,
Expand Down
2 changes: 1 addition & 1 deletion packages/code/src/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { constants } from 'node:fs';
import { open, lstat, realpath, stat } from 'node:fs/promises';
import { open, lstat, realpath, stat } from './root-access.js';
import { createHash } from 'node:crypto';
import { resolve, relative, isAbsolute, sep } from 'node:path';

Expand Down
2 changes: 2 additions & 0 deletions packages/code/src/native-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export class NativeProcessWorkspaceCommandSandbox implements WorkspaceCommandSan
child.on('disconnect', lost);
const {
workspaceRoot,
workspaceIdentity,
commandPolicy,
protectedPaths,
allowedDomains,
Expand All @@ -348,6 +349,7 @@ export class NativeProcessWorkspaceCommandSandbox implements WorkspaceCommandSan
{
options: {
workspaceRoot,
workspaceIdentity,
commandPolicy,
protectedPaths,
allowedDomains,
Expand Down
10 changes: 7 additions & 3 deletions packages/code/src/native-programmatic-live.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import type { AddressInfo } from 'node:net';
import { NativeProcessWorkspaceCommandSandbox } from './native-process.js';
import { resolveNativeSrtCommandPolicy } from './native-policy.js';

test('real SRT prevents speculative network effects under trusted-vm', {
for (const selected of [false, true]) {
test(`real SRT prevents speculative network effects under trusted-vm${selected ? ' for a selected project' : ''}`, {
skip: process.env.LIBRECHAT_CODE_LIVE_SRT_TESTS !== '1',
timeout: 30_000,
}, async () => {
const root = await mkdtemp(join(tmpdir(), 'native-ptc-effects-'));
const root = await realpath(await mkdtemp(join(tmpdir(), 'native-ptc-effects-')));
const identity = await stat(root, { bigint: true });
let effects = 0;
const server = createServer((_req, res) => { effects += 1; res.end('ok'); });
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
const port = (server.address() as AddressInfo).port;
const executor = new NativeProcessWorkspaceCommandSandbox({
workspaceRoot: root,
...(selected ? { workspaceIdentity: { path: root, dev: String(identity.dev), ino: String(identity.ino) } } : {}),
commandPolicy: resolveNativeSrtCommandPolicy('trusted-vm'),
programmaticFileUpstream: `http://127.0.0.1:${port}`,
});
Expand All @@ -39,3 +42,4 @@ test('real SRT prevents speculative network effects under trusted-vm', {
}
}
});
}
114 changes: 114 additions & 0 deletions packages/code/src/native-sandbox.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
import {
access,
chmod,
Expand Down Expand Up @@ -236,6 +237,119 @@ test('programmatic probes use a copy-on-write workspace without mutating the pro
assert.equal(await readFile(join(snapshot, 'state.txt'), 'utf8'), 'probe-only');
});

test('selected command cwd stays bound when replacement happens while wrapping', async t => {
const parent = await mkdtemp(join(tmpdir(), 'librechat-project-command-'));
t.after(() => rm(parent, { recursive: true, force: true }));
const root = join(await realpath(parent), 'project');
await mkdir(root);
await writeFile(join(root, 'identity.txt'), 'original');
const identity = await stat(root, { bigint: true });
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
workspaceIdentity: { path: root, dev: String(identity.dev), ino: String(identity.ino) },
manager: fakeManager({ beforeWrap: async () => {
await rename(root, `${root}.old`);
await mkdir(root);
await writeFile(join(root, 'identity.txt'), 'replacement');
} }).manager,
});
t.after(() => sandbox.close());
const result = await sandbox.execute({ ...request, command: 'cat identity.txt; printf written > result.txt' });
assert.equal(result.exitCode, 0, result.stderr);
assert.equal(result.stdout, 'original');
assert.equal(await readFile(join(`${root}.old`, 'result.txt'), 'utf8'), 'written');
await assert.rejects(access(join(root, 'result.txt')));
});

test('selected command cancellation kills the exec trampoline process group', async t => {
const parent = await mkdtemp(join(tmpdir(), 'librechat-project-cancel-'));
t.after(() => rm(parent, { recursive: true, force: true }));
const root = await realpath(parent);
const identity = await stat(root, { bigint: true });
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
workspaceIdentity: { path: root, dev: String(identity.dev), ino: String(identity.ino) },
manager: fakeManager().manager,
});
t.after(() => sandbox.close());
const controller = new AbortController();
const running = sandbox.execute({ ...request, timeoutMs: 5000,
command: 'printf started > started; sleep 3; printf late > late' }, controller.signal);
const rejected = assert.rejects(running, error => error instanceof WorkspaceToolError && error.code === 'EXECUTION_ABORTED');
const deadline = Date.now() + 3000;
while (true) {
try { await access(join(root, 'started')); break; } catch { /* Wait for the actual child. */ }
if (Date.now() > deadline) throw new Error('Selected command did not start');
await new Promise(resolve => setTimeout(resolve, 20));
}
controller.abort();
await rejected;
await new Promise(resolve => setTimeout(resolve, 3100));
await assert.rejects(access(join(root, 'late')));
});

test('selected replay copy stays on the verified directory after pathname replacement', async t => {
const parent = await mkdtemp(join(tmpdir(), 'librechat-project-copy-'));
t.after(() => rm(parent, { recursive: true, force: true }));
const root = join(await realpath(parent), 'project');
await mkdir(root);
await writeFile(join(root, 'identity.txt'), 'original');
const identity = await stat(root, { bigint: true });
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
workspaceIdentity: { path: root, dev: identity.dev.toString(), ino: identity.ino.toString() },
manager: fakeManager().manager,
spawnCommand(command, args, options) {
assert.equal(command, process.execPath);
renameSync(root, `${root}.old`);
mkdirSync(root);
writeFileSync(join(root, 'identity.txt'), 'replacement');
return spawn(command, args, options);
},
});
t.after(() => sandbox.close());
const directory = await sandbox.createExecutionDirectory();
let snapshot: string;
try {
snapshot = await sandbox.createProgrammaticProbeWorkspace(directory);
} catch (error) {
if (error instanceof CopyOnWriteCloneUnavailableError) {
t.skip('host filesystem does not support copy-on-write cloning');
return;
}
throw error;
}
assert.equal(await readFile(join(root, 'identity.txt'), 'utf8'), 'replacement');
assert.equal(await readFile(join(snapshot, 'identity.txt'), 'utf8'), 'original');
});

test('programmatic probes reject a replaced selected project before copying', async t => {
const parent = await mkdtemp(join(tmpdir(), 'librechat-project-probe-'));
t.after(() => rm(parent, { recursive: true, force: true }));
const root = join(await realpath(parent), 'project');
await mkdir(root);
const identity = await stat(root, { bigint: true });
let copies = 0;
const sandbox = new NativeSrtWorkspaceCommandSandbox({
workspaceRoot: root,
workspaceIdentity: { path: root, dev: identity.dev.toString(), ino: identity.ino.toString() },
manager: fakeManager().manager,
spawnCommand() {
copies++;
throw new Error('must not copy a replaced project');
},
});
t.after(() => sandbox.close());
const executionDirectory = await sandbox.createExecutionDirectory();
await rename(root, join(parent, 'original'));
await mkdir(root);
await assert.rejects(
sandbox.createProgrammaticProbeWorkspace(executionDirectory),
/Selected project changed before probe staging/,
);
assert.equal(copies, 0);
});

test('programmatic probes do not hide clone implementation failures as unsupported filesystems', async t => {
const root = await mkdtemp(join(tmpdir(), 'librechat-code-native-'));
t.after(() => rm(root, { recursive: true, force: true }));
Expand Down
Loading