Skip to content
Open
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
36 changes: 34 additions & 2 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { readdir, realpath, stat } from 'node:fs/promises';
import { basename, dirname, join, resolve, relative, isAbsolute, sep } from 'node:path';

import { pairBridgeWorker } from './pairing.js';
import { runSetupCommand } from './setup-cli.js';
import { profileRunConfiguration } from './setup.js';
import { assertWorkerProfileIsolation } from './worker-profile.js';
import type { WorkerProfile } from './worker-profile.js';
import { discoverProjects } from './projects.js';
import { loadProjectRoots, projectRootArguments } from './project-roots.js';
import {
Expand Down Expand Up @@ -358,6 +362,7 @@ async function relay(): Promise<void> {
async function run(
runtimeSessionId?: string,
args: string[] = [],
savedProfile?: { path: string; config: WorkerProfile },
): Promise<void> {
const projectArgs = projectRootArguments(args);
if (
Expand Down Expand Up @@ -422,7 +427,7 @@ async function run(
}
const configuredWorkerId = process.env.LIBRECHAT_CODE_WORKER_ID?.trim();
const configuredIdentityPath =
process.env.LIBRECHAT_CODE_IDENTITY_FILE?.trim();
savedProfile?.config.identityPath ?? process.env.LIBRECHAT_CODE_IDENTITY_FILE?.trim();
const configuredToken = process.env.LIBRECHAT_CODE_WORKER_TOKEN?.trim();
const identityPath =
configuredIdentityPath ??
Expand Down Expand Up @@ -645,6 +650,9 @@ async function run(
throw new Error('Invalid workspace registration');
}
}
if (savedProfile) {
await assertWorkerProfileIsolation(savedProfile.path, identityPath!, canonicalWorkerDirectory);
}
const mutationQuarantinePath =
(allowWorkspaceWrites || allowWorkspaceCommands) &&
canonicalWorkerDirectory
Expand Down Expand Up @@ -1092,6 +1100,7 @@ async function run(
commandPolicy,
protectedPaths: [
identityPath,
savedProfile?.path,
...(environments.some(environment => environment.definition.setup?.reuse) ? [preparationDirectory] : []),
...snapshotStores.map(store => store.store),
...environments.map(environment => environment.path),
Expand Down Expand Up @@ -1766,6 +1775,26 @@ async function clearMutationQuarantine(args: string[]): Promise<void> {

async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 1 && (args[0] === '--help' || args[0] === 'help')) {
process.stdout.write(`Usage: librechat-code <command>

setup Guided pairing and private profile setup
run [--profile <file>] Start a saved profile or existing worker configuration
pair <url> <code> --worker-id <id> Existing one-time pairing flow
projects --root <directory> Inspect local Git projects
reset-workspace <id> Existing runtime maintenance
clear-workspace-quarantine Existing workspace maintenance

Run librechat-code setup --help for setup options.
Legacy LIBRECHAT_CODE_* configuration takes precedence over the default profile.
Explicit profiles cannot be mixed with legacy settings or run options.
`);
return;
}
if (args[0] === 'setup') {
await runSetupCommand(args.slice(1));
return;
}
if (args[0] === 'prune-environment-storage') {
const paths: string[] = [];
for (let i = 1; i < args.length; i++) {
Expand Down Expand Up @@ -1823,7 +1852,10 @@ async function main(): Promise<void> {
if (args[0] && args[0] !== 'run') {
throw new Error(`Unknown command: ${args[0]}`);
}
await run(undefined, args.slice(1));
const configuration = await profileRunConfiguration(args.slice(1));
await run(undefined, configuration.args, configuration.profile && configuration.profilePath
? { path: configuration.profilePath, config: configuration.profile }
: undefined);
}
main().catch((error: Error) => {
process.stderr.write(`librechat-code: ${error.message}\n`);
Expand Down
58 changes: 58 additions & 0 deletions packages/code/src/pairing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,61 @@ test('pairing binds a generated worker key to a single-use code', async () => {
assert.match(paired.publicKey, /BEGIN PUBLIC KEY/);
assert.match(paired.privateKey, /BEGIN PRIVATE KEY/);
});

test('pairing propagates cancellation and never follows a code-bearing redirect', async () => {
const controller = new AbortController();
const paired = await pairBridgeWorker({
codeApiUrl: 'https://code.example/v1',
workerId: 'vm-1',
code: 'one-time-code',
signal: controller.signal,
fetchImpl: async (_input, init) => {
assert.equal(init?.redirect, 'error');
assert.equal(init?.signal, controller.signal);
return Response.json({
protocolVersion: 1,
workerId: 'vm-1',
credential: 'issued-short-lived-credential-value',
expiresAt: new Date(Date.now() + 300_000).toISOString(),
});
},
});
assert.equal(paired.workerId, 'vm-1');
});

for (const payload of [
null,
[],
'not a response',
{ protocolVersion: 2 },
{ protocolVersion: 1, workerId: 'another-worker' },
]) {
test(`pairing rejects malformed response ${JSON.stringify(payload)}`, async () => {
await assert.rejects(
pairBridgeWorker({
codeApiUrl: 'https://code.example/v1',
workerId: 'vm-1',
code: 'one-time-code',
fetchImpl: async () => Response.json(payload),
}),
/invalid/,
);
});
}

test('non-JSON pairing failures have a bounded diagnosis without echoing response contents', async () => {
await assert.rejects(
pairBridgeWorker({
codeApiUrl: 'https://code.example/v1',
workerId: 'vm-1',
code: 'one-time-code',
fetchImpl: async () =>
new Response('private upstream response', { status: 502 }),
}),
error => {
assert.match(String(error), /invalid pairing response/);
assert.ok(!String(error).includes('private upstream response'));
return true;
},
);
});
39 changes: 30 additions & 9 deletions packages/code/src/pairing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { createBridgeIdentity } from './identity.js';
import {
BRIDGE_PROTOCOL_VERSION,
BridgeProtocolError,
} from './protocol.js';
import { BRIDGE_PROTOCOL_VERSION, BridgeProtocolError } from './protocol.js';

import type { BridgeWorkerCredentialResponse } from './protocol.js';

Expand All @@ -11,6 +8,7 @@ export interface PairBridgeWorkerOptions {
workerId: string;
code: string;
fetchImpl?: typeof fetch;
signal?: AbortSignal;
}

export interface PairedBridgeWorkerIdentity
Expand All @@ -24,8 +22,8 @@ function normalizedBaseUrl(value: string): string {
return value.replace(/\/+$/, '');
}

function errorMessage(value: object): string | undefined {
if ('error' in value && typeof value.error === 'string') return value.error;
function errorMessage(value: Record<string, unknown>): string | undefined {
if (typeof value.error === 'string') return value.error;
return undefined;
}

Expand All @@ -38,6 +36,8 @@ export async function pairBridgeWorker(
`${codeApiUrl}/bridge/pairings/redeem`,
{
method: 'POST',
redirect: 'error',
signal: options.signal,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
protocolVersion: BRIDGE_PROTOCOL_VERSION,
Expand All @@ -47,10 +47,29 @@ export async function pairBridgeWorker(
}),
},
);
const payload = (await response.json()) as object;
let payload: unknown;
try {
payload = await response.json();
} catch {
throw new BridgeProtocolError(
'Code API returned an invalid pairing response',
response.status,
);
}
if (
typeof payload !== 'object' ||
payload == null ||
Array.isArray(payload)
) {
throw new BridgeProtocolError(
'Code API returned an invalid pairing response',
response.status,
);
}
if (!response.ok) {
throw new BridgeProtocolError(
errorMessage(payload) ?? `Bridge pairing failed with HTTP ${response.status}`,
errorMessage(payload as Record<string, unknown>) ??
`Bridge pairing failed with HTTP ${response.status}`,
response.status,
);
}
Expand All @@ -62,7 +81,9 @@ export async function pairBridgeWorker(
credential.credential.length < 32 ||
!Number.isFinite(Date.parse(credential.expiresAt))
) {
throw new BridgeProtocolError('Code API returned an invalid worker credential');
throw new BridgeProtocolError(
'Code API returned an invalid worker credential',
);
}
return {
...credential,
Expand Down
Loading
Loading