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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ SANDBOX_OUTPUT_MAX_SIZE=65536
# CODEAPI_RUNTIME_SESSION_MODE=affinity
# CODEAPI_BRIDGE_WORKER_ID=my-vm
# CODEAPI_BRIDGE_TOKEN=replace-with-a-strong-random-secret
# CODEAPI_BRIDGE_AUTH_MODE=paired

# Service Configuration
PYTHON_CONCURRENCY=5
Expand Down
86 changes: 86 additions & 0 deletions docs/adr/001-stateful-code-environments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# ADR 001: Stateful code environments use an outbound Code API bridge

- Status: Accepted for alpha
- Date: 2026-08-30

## Context

LibreChat needs coding agents to reuse a workspace across conversation turns
while allowing the environment owner to choose the VM. Internet-facing
LibreChat instances cannot safely require inbound access to that VM, forward
end-user tokens to it, or treat an MCP connection as a sandbox boundary.

The first alpha demonstrated a stable runtime-session ID, a single fenced
worker lease, and workspace persistence across turns. Its static shared worker
token was sufficient to prove execution flow but is not an acceptable hardened
enrollment mechanism.

## Decision

The product concept is a **stateful code environment**. Code API remains its
broker and policy boundary, and `remote-bridge` is a Code API sandbox backend.
The `@librechat/code` worker connects outbound from the chosen VM and forwards
assignments only to a loopback or private sandbox endpoint.

Hardened workers enroll through a one-time pairing code:

1. An administrator creates a code scoped to the configured worker ID.
2. The CLI generates an Ed25519 keypair locally and redeems the code with only
its public key.
3. Code API returns a fifteen-minute credential bound to that public key.
4. Every worker request signs the method, path, body digest, timestamp, nonce,
and credential.
5. Code API rejects stale timestamps and replayed nonces and supports rotation
and immediate revocation.

Static bearer authentication remains a non-hardened compatibility mode.

## Ownership and state

The alpha environment is deployment/operator owned and configured with one
worker ID. A future LibreChat control plane may persist deployment-, tenant-,
or user-owned environment records and issue the same pairing operation through
RBAC-protected APIs without changing the worker execution protocol.

Workspace state belongs to the stable runtime session, not to a transient
assignment lease. For `remote-bridge`, that state currently survives turns on
the same worker and backing disk. It is not yet checkpointed or portable across
worker replacement; the UI and operator documentation must not imply otherwise.

## Security invariants

- The VM requires no inbound internet listener.
- Code API, not the worker, authenticates LibreChat users and normalizes work.
- A stolen short-lived credential is insufficient without the worker private
key; a stolen private key is insufficient after credential expiry or
revocation.
- Pairing codes and credentials are stored by digest where lookup permits.
- One configured worker has at most one active fenced assignment.
- Sandbox isolation and default-deny egress remain mandatory; pairing secures
the transport identity but does not make the host a sandbox.
- A compromised worker can lie about advertised capabilities. Capability
labels and policy digests are audit signals until enforcement is coupled to
an attested sandbox or trusted host policy.

## Consequences

- `@librechat/code` owns the provider-neutral protocol, identity handling, and
worker CLI; Code API owns enrollment, scheduling, and execution policy.
- LibreChat owns environment persistence, ownership, RBAC, and user experience.
- The Agents SDK keeps only its adapter until a second concrete consumer proves
which coding-tool abstractions are genuinely provider neutral.
- MCP may expose environment operations later, but it is not the worker
transport or isolation boundary.
- Multi-worker directories, checkpoint/restore, owner-scoped quotas, and
enforced network capability profiles remain follow-up decisions.

## Alternatives rejected

- **Inbound SSH/HTTP to the VM:** expands attack surface and complicates NAT and
firewall operation.
- **MCP as the worker protocol:** conflates tool discovery with leases,
cancellation, fencing, and sandbox policy.
- **Put the runtime in the Agents SDK:** couples provider-neutral execution to
one agent integration and makes non-agent consumers depend on agent internals.
- **Long-lived shared bearer token:** easy to bootstrap, but replayable and not
bound to a worker-held key.
36 changes: 29 additions & 7 deletions docs/remote-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ LibreChat -> Code API -> Redis assignment

Code API remains the public authentication, policy, manifest, timeout, and
result-normalization boundary. The bridge worker has a separate operator
credential and never accepts end-user bearer tokens directly.
identity and never accepts end-user bearer tokens directly.

## Code API configuration

Expand All @@ -23,16 +23,30 @@ CODEAPI_SANDBOX_BACKEND=remote-bridge
CODEAPI_EXECUTION_PROFILE=stateful
CODEAPI_RUNTIME_SESSION_MODE=affinity
CODEAPI_BRIDGE_WORKER_ID=my-vm
CODEAPI_BRIDGE_TOKEN=<strong-random-secret>
CODEAPI_BRIDGE_TOKEN=<strong-administrator-bootstrap-secret>
CODEAPI_BRIDGE_AUTH_MODE=paired
```

Use `strict` instead of `affinity` if every request must include a runtime
session hint. In hardened mode, startup requires the bridge token to be at least
32 bytes. `PTC_MODE=blocking` is rejected; replay mode is required because a
remote execution cannot retain an open Code API process across tool callbacks.

Start the CLI beside a sandbox using the same worker ID and secret; see
[`@librechat/code`](../../packages/code/README.md).
Create a single-use pairing code with the administrator secret:

```bash
curl -fsS https://code.example.com/v1/bridge/pairings \
-H "Authorization: Bearer $CODEAPI_BRIDGE_TOKEN" \
-H 'Content-Type: application/json' \
--data '{"workerId":"my-vm"}'
```

Redeem the returned code on the VM using
[`@librechat/code`](../../packages/code/README.md). The CLI generates its key
locally, proves possession on every request, and rotates its short-lived
credential before expiry. `CODEAPI_BRIDGE_AUTH_MODE=static` remains available
for non-hardened development compatibility only.

Stateful deployments must also set `LIBRECHAT_CODE_STATEFUL_WORKSPACE=true`
and route the CLI's `{runtimeSessionId}` endpoint template to an isolated,
persistent local runner per session. A single sandbox endpoint is stateless and
Expand Down Expand Up @@ -63,6 +77,13 @@ execution.
## Lifecycle and fencing

- Registration is ephemeral in Redis and must be refreshed by the worker.
- Pairing codes are stored hashed, expire after ten minutes, and are consumed
atomically on their first redemption attempt.
- Worker credentials expire after fifteen minutes and are bound to an Ed25519
public key. Exact-request signatures include the HTTP method, path, body
digest, timestamp, nonce, and credential.
- Accepted proof nonces cannot be replayed, credentials rotate before expiry,
and an administrator can revoke the active worker identity immediately.
- Code API permits one active assignment per configured worker.
- Each assignment has an absolute deadline, generation, and random lease token.
- Settlements with the wrong worker, generation, token, or expired deadline are
Expand All @@ -79,9 +100,10 @@ For internet-facing LibreChat deployments, use the hardened microVM/NsJail
stack, default-deny sandbox egress, signed execution manifests, least-privilege
host credentials, resource limits, and host/network monitoring. Bind the local
sandbox endpoint to loopback or a private container network. Rotate a leaked
bridge token immediately; the initial protocol intentionally uses a static
operator secret and supports one configured worker per Code API deployment.
administrator token immediately. Pairing secures worker transport identity; it
cannot attest that a compromised VM truthfully reports or enforces its sandbox
capabilities.

The next control-plane layer can add short-lived pairing credentials and a
The next control-plane layer can add owner-scoped environment records and a
multi-worker directory without changing the execution protocol or moving code
tools into the Agents SDK.
40 changes: 35 additions & 5 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,37 @@ untrusted internet traffic). It connects outbound to Code API, long-polls for
assignments, forwards them to the local sandbox, and returns fenced results.
The VM does not need an inbound public port.

## Run
## Pair

Hardened deployments use a one-time code instead of copying a long-lived
worker secret onto the VM. After an administrator creates a code, run:

```bash
librechat-code pair https://code.example.com/v1 '<one-time-code>' \
--worker-id my-vm
```

The CLI generates an Ed25519 key locally and writes its paired identity to
`~/.config/librechat/code/my-vm.json` with owner-only permissions. The private
key never leaves the VM. Worker requests carry an exact-request signature,
timestamp, and one-time nonce; the short-lived credential rotates
automatically.

Then start the worker without a shared secret:

```bash
LIBRECHAT_CODE_WORKER_ID=my-vm \
LIBRECHAT_CODE_SANDBOX_ENDPOINT=http://127.0.0.1:2000/api/v2 \
librechat-code run
```

Use `--identity <path>` while pairing and
`LIBRECHAT_CODE_IDENTITY_FILE=<path>` while running to override the identity
file location.

## Static compatibility mode

Non-hardened development deployments may still run with a static token:

```bash
npm install -g @librechat/code
Expand All @@ -18,7 +48,7 @@ LIBRECHAT_CODE_URL=https://code.example.com/v1 \
LIBRECHAT_CODE_WORKER_TOKEN='<strong random secret>' \
LIBRECHAT_CODE_WORKER_ID=my-vm \
LIBRECHAT_CODE_SANDBOX_ENDPOINT=http://127.0.0.1:2000/api/v2 \
librechat-code
librechat-code run
```

Optional environment variables:
Expand All @@ -38,6 +68,6 @@ A single built-in sandbox runner binds itself to one runtime session and must
not be advertised as stateful. Use the default stateless capability until a
session-routing supervisor is configured.

Use a unique worker ID and secret per Code API deployment, expose only the
sandbox loopback endpoint to the CLI, and enforce VM/container egress policy
independently of the bridge transport.
Static worker authentication is rejected when Code API hardened mode is
enabled. Expose only the sandbox loopback endpoint to the CLI, and enforce
VM/container egress policy independently of the bridge transport.
157 changes: 123 additions & 34 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto';

import { pairBridgeWorker } from './pairing.js';
import {
defaultBridgeIdentityPath,
loadBridgeIdentity,
saveBridgeIdentity,
} from './storage.js';
import { BridgeWorker } from './worker.js';

function required(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required`);
return value;
function required(name: string, value = process.env[name]): string {
const normalized = value?.trim();
if (!normalized) throw new Error(`${name} is required`);
return normalized;
}

function list(value: string | undefined): string[] {
Expand All @@ -17,39 +24,121 @@ function list(value: string | undefined): string[] {
);
}

const controller = new AbortController();
process.once('SIGINT', () => controller.abort());
process.once('SIGTERM', () => controller.abort());

const policy = process.env.LIBRECHAT_CODE_POLICY ?? 'default-deny';
const statefulWorkspace =
process.env.LIBRECHAT_CODE_STATEFUL_WORKSPACE?.trim().toLowerCase() === 'true';
const sandboxEndpoint =
process.env.LIBRECHAT_CODE_SANDBOX_ENDPOINT ??
'http://127.0.0.1:2000/api/v2';
if (statefulWorkspace && !sandboxEndpoint.includes('{runtimeSessionId}')) {
throw new Error(
'LIBRECHAT_CODE_STATEFUL_WORKSPACE requires LIBRECHAT_CODE_SANDBOX_ENDPOINT to contain {runtimeSessionId}',
function option(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index >= 0) return args[index + 1];
return args.find((value) => value.startsWith(`${name}=`))?.slice(name.length + 1);
}

async function pair(args: string[]): Promise<void> {
const codeApiUrl = required('instance URL', args[1]);
const code = required('one-time pairing code', args[2]);
const workerId = required(
'--worker-id or LIBRECHAT_CODE_WORKER_ID',
option(args, '--worker-id') ?? process.env.LIBRECHAT_CODE_WORKER_ID,
);
const identityPath =
option(args, '--identity') ??
process.env.LIBRECHAT_CODE_IDENTITY_FILE ??
defaultBridgeIdentityPath(workerId);
const identity = await pairBridgeWorker({ codeApiUrl, workerId, code });
await saveBridgeIdentity(identityPath, identity);
process.stdout.write(
`Paired worker ${workerId}. Identity saved to ${identityPath}\n`,
);
}
const worker = new BridgeWorker({
codeApiUrl: required('LIBRECHAT_CODE_URL'),
token: required('LIBRECHAT_CODE_WORKER_TOKEN'),
workerId: required('LIBRECHAT_CODE_WORKER_ID'),
sandboxEndpoint,
capabilities: {
statefulWorkspace,
sandboxProfile: process.env.LIBRECHAT_CODE_SANDBOX_PROFILE ?? 'nsjail',
runtimes: list(process.env.LIBRECHAT_CODE_RUNTIMES),
policyDigest: createHash('sha256').update(policy).digest('hex'),
},
onError: (error) => {
const message = error instanceof Error ? error.message : 'unknown bridge error';
process.stderr.write(`librechat-code: reconnecting after ${message}\n`);
},
});

worker.run(controller.signal).catch((error: Error) => {
async function run(): 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,
);
if (pairedIdentity && pairedIdentity.workerId !== workerId) {
throw new Error(
`Identity belongs to ${pairedIdentity.workerId}, not configured worker ${workerId}`,
);
}
const codeApiUrl = required(
'LIBRECHAT_CODE_URL',
process.env.LIBRECHAT_CODE_URL ?? pairedIdentity?.codeApiUrl,
);
const policy = process.env.LIBRECHAT_CODE_POLICY ?? 'default-deny';
const statefulWorkspace =
process.env.LIBRECHAT_CODE_STATEFUL_WORKSPACE?.trim().toLowerCase() ===
'true';
const sandboxEndpoint =
process.env.LIBRECHAT_CODE_SANDBOX_ENDPOINT ??
'http://127.0.0.1:2000/api/v2';
if (statefulWorkspace && !sandboxEndpoint.includes('{runtimeSessionId}')) {
throw new Error(
'LIBRECHAT_CODE_STATEFUL_WORKSPACE requires LIBRECHAT_CODE_SANDBOX_ENDPOINT to contain {runtimeSessionId}',
);
}
const workerIdentity = pairedIdentity
? {
privateKey: pairedIdentity.privateKey,
credential: pairedIdentity.credential,
expiresAt: pairedIdentity.expiresAt,
}
: undefined;
const controller = new AbortController();
process.once('SIGINT', () => controller.abort());
process.once('SIGTERM', () => controller.abort());
const worker = new BridgeWorker({
codeApiUrl,
token: configuredToken,
identity: workerIdentity,
workerId,
sandboxEndpoint,
capabilities: {
statefulWorkspace,
sandboxProfile: process.env.LIBRECHAT_CODE_SANDBOX_PROFILE ?? 'nsjail',
runtimes: list(process.env.LIBRECHAT_CODE_RUNTIMES),
policyDigest: createHash('sha256').update(policy).digest('hex'),
},
onIdentityChange:
pairedIdentity && identityPath
? async (identity) => {
await saveBridgeIdentity(identityPath, {
...pairedIdentity,
credential: identity.credential,
expiresAt: identity.expiresAt,
});
}
: undefined,
onError: (error) => {
const message =
error instanceof Error ? error.message : 'unknown bridge error';
process.stderr.write(`librechat-code: reconnecting after ${message}\n`);
},
});
await worker.run(controller.signal);
}

async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args[0] === 'pair') {
await pair(args);
return;
}
if (args[0] && args[0] !== 'run') {
throw new Error(`Unknown command: ${args[0]}`);
}
await run();
}

main().catch((error: Error) => {
process.stderr.write(`librechat-code: ${error.message}\n`);
process.exitCode = 1;
});
Loading