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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ SANDBOX_RUN_CPU_TIME=10000
SANDBOX_RUN_TIMEOUT=15000
SANDBOX_OUTPUT_MAX_SIZE=65536

# Docker Compose keeps hardened mode enabled. Its API exposes bridge routes even
# with the default HTTP sandbox backend, so configure a private enrollment token
# of at least 32 bytes before starting Compose (generate with: openssl rand -hex 32).
# Do not use a shared example token. These defaults accept paired dynamic workers;
# enrollment still requires the token. Never provide this token to sandbox code.
CODEAPI_BRIDGE_TOKEN=
CODEAPI_BRIDGE_AUTH_MODE=paired
CODEAPI_BRIDGE_DYNAMIC_WORKERS=true
# For a fixed worker, set DYNAMIC_WORKERS=false and WORKER_ID to that worker's ID.
CODEAPI_BRIDGE_WORKER_ID=

# Remote stateful code bridge (Code API deployment)
# CODEAPI_SANDBOX_BACKEND=remote-bridge
# CODEAPI_EXECUTION_PROFILE=stateful
# 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Bridge pairing rollout safety
run: tests/bridge_pairing_rollout.sh

- name: Compose bridge configuration
run: node tests/compose-bridge-config.cjs

- name: Validate sandbox Dockerfiles
run: |
docker buildx build --check -f api/Dockerfile .
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ cut.

## Local Development

Copy `.env.example` to `.env` and set `CODEAPI_BRIDGE_TOKEN` to a private value
of at least 32 bytes (generate one with `openssl rand -hex 32`). The API exposes
bridge routes even with the default HTTP sandbox backend, so hardened mode
requires this enrollment credential. Compose defaults to
`CODEAPI_BRIDGE_AUTH_MODE=paired` and `CODEAPI_BRIDGE_DYNAMIC_WORKERS=true`.
To restrict pairing to a fixed worker, set `CODEAPI_BRIDGE_DYNAMIC_WORKERS=false`
and `CODEAPI_BRIDGE_WORKER_ID` to its ID. Keep the token outside workspaces and
model-visible configuration.

```bash
docker-compose up --build
```
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ services:
environment:
- LOCAL_MODE=${LOCAL_MODE:-true}
- CODEAPI_HARDENED_SANDBOX_MODE=${CODEAPI_HARDENED_SANDBOX_MODE:-true}
- CODEAPI_BRIDGE_TOKEN=${CODEAPI_BRIDGE_TOKEN:-}
- CODEAPI_BRIDGE_AUTH_MODE=${CODEAPI_BRIDGE_AUTH_MODE:-paired}
- CODEAPI_BRIDGE_DYNAMIC_WORKERS=${CODEAPI_BRIDGE_DYNAMIC_WORKERS:-true}
- CODEAPI_BRIDGE_WORKER_ID=${CODEAPI_BRIDGE_WORKER_ID:-}
- CODEAPI_AUTH_PROVIDER=${CODEAPI_AUTH_PROVIDER:-}
- CODEAPI_ALLOW_AUTH_PROVIDER_NONE=${CODEAPI_ALLOW_AUTH_PROVIDER_NONE:-}
- CODEAPI_JWT_ISSUER=${CODEAPI_JWT_ISSUER:-}
Expand Down Expand Up @@ -54,6 +58,10 @@ services:
environment:
- LOCAL_MODE=${LOCAL_MODE:-true}
- CODEAPI_HARDENED_SANDBOX_MODE=${CODEAPI_HARDENED_SANDBOX_MODE:-true}
- CODEAPI_BRIDGE_TOKEN=${CODEAPI_BRIDGE_TOKEN:-}
- CODEAPI_BRIDGE_AUTH_MODE=${CODEAPI_BRIDGE_AUTH_MODE:-paired}
- CODEAPI_BRIDGE_DYNAMIC_WORKERS=${CODEAPI_BRIDGE_DYNAMIC_WORKERS:-true}
- CODEAPI_BRIDGE_WORKER_ID=${CODEAPI_BRIDGE_WORKER_ID:-}
- CODEAPI_AUTH_PROVIDER=${CODEAPI_AUTH_PROVIDER:-}
- CODEAPI_JWT_SINGLE_TENANT_ID=${CODEAPI_JWT_SINGLE_TENANT_ID:-}
- CODEAPI_TENANT_ISOLATION_STRICT=${CODEAPI_TENANT_ISOLATION_STRICT:-}
Expand Down
41 changes: 41 additions & 0 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,47 @@ policy: an allowed destination can receive workspace data. The normalized
allowlist is included in the worker policy digest. Tool approval hooks remain
the user-facing allow/deny boundary for each invocation.

### GitHub authentication

The native BYOM worker can provide Git HTTPS authentication without exposing a
real token to the command sandbox. Prefer a GitHub App installed only on the
repositories the agent may access:

```bash
LIBRECHAT_CODE_GITHUB_APP_ID=12345 \
LIBRECHAT_CODE_GITHUB_INSTALLATION_ID=67890 \
LIBRECHAT_CODE_GITHUB_PRIVATE_KEY_FILE=/secure/librechat-agent.pem \
librechat-code run --worker-dir /path/to/project --allow-workspace-commands
```

The private key must be an owner-only regular file outside the workspace. It is
read only by the trusted worker, which mints and refreshes short-lived
installation tokens. A personal access token is supported as a fallback with
`LIBRECHAT_CODE_GITHUB_TOKEN`, but the GitHub App is the safer default because
its repository access and permissions can be narrowly installed and revoked.
Native Windows currently requires token mode because the worker cannot
reliably validate private-key ACLs there; use WSL2 for GitHub App mode.

Git receives authentication through process-scoped `GIT_CONFIG_*` variables.
The same isolated config supplies the standard Git LFS filters; hosts using LFS
must install `git-lfs`, and checkout fails instead of silently leaving pointer
files when it is unavailable.
SRT replaces only the bearer-token portion with a sentinel inside the sandbox
and substitutes the real value in its host proxy only for `github.com` HTTPS
traffic. TLS termination is enabled for that substitution. The worker restores
the parent environment immediately after constructing the sandbox command; it
never writes credentials into the repository, a remote URL, or Git config.
GitHub's required domains are added to the command egress allowlist only when
authentication is configured. The worker identity, GitHub App key path, token
source variables, and mutation-quarantine record remain denied to sandboxed
commands.

For GitHub Enterprise Server, set `LIBRECHAT_CODE_GITHUB_HOST` to its hostname
and `LIBRECHAT_CODE_GITHUB_API_URL` to its HTTPS API base URL. GitHub
authentication currently requires the `native-srt` command sandbox. Every
clone, commit, or push command still crosses LibreChat's tool-approval policy;
the credential boundary does not grant approval by itself.

Select the backend explicitly when desired:

```bash
Expand Down
4 changes: 4 additions & 0 deletions packages/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"./native-sandbox": {
"types": "./dist/native-sandbox.d.ts",
"import": "./dist/native-sandbox.js"
},
"./github": {
"types": "./dist/github.d.ts",
"import": "./dist/github.js"
}
},
"bin": {
Expand Down
114 changes: 112 additions & 2 deletions packages/code/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,105 @@ test('CLI rejects an unknown command sandbox before entering the run loop', () =
);
});

test('CLI rejects incomplete GitHub App authentication before worker registration', () => {
const result = spawnSync(
process.execPath,
[fileURLToPath(new URL('./cli.js', import.meta.url))],
{
encoding: 'utf8',
env: {
...process.env,
LIBRECHAT_CODE_URL: 'https://code.example/v1',
LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret',
LIBRECHAT_CODE_WORKER_ID: 'engineering-vm',
LIBRECHAT_CODE_GITHUB_APP_ID: '123',
LIBRECHAT_CODE_GITHUB_INSTALLATION_ID: undefined,
LIBRECHAT_CODE_GITHUB_PRIVATE_KEY_FILE: undefined,
},
},
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /GitHub App authentication requires/);
});

test('CLI refuses GitHub credentials without native sandboxed commands', () => {
const result = spawnSync(
process.execPath,
[fileURLToPath(new URL('./cli.js', import.meta.url))],
{
encoding: 'utf8',
env: {
...process.env,
LIBRECHAT_CODE_URL: 'https://code.example/v1',
LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret',
LIBRECHAT_CODE_WORKER_ID: 'engineering-vm',
LIBRECHAT_CODE_GITHUB_TOKEN: 'github_pat_abcdefghijklmnopqrstuvwxyz',
},
},
);

assert.notEqual(result.status, 0);
assert.match(
result.stderr,
/GitHub authentication requires workspace commands/,
);
});

test('CLI rejects a GitHub App API URL that does not match its Git host', () => {
const result = spawnSync(
process.execPath,
[fileURLToPath(new URL('./cli.js', import.meta.url))],
{
encoding: 'utf8',
env: {
...process.env,
LIBRECHAT_CODE_URL: 'https://code.example/v1',
LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret',
LIBRECHAT_CODE_WORKER_ID: 'engineering-vm',
LIBRECHAT_CODE_GITHUB_APP_ID: '123',
LIBRECHAT_CODE_GITHUB_INSTALLATION_ID: '456',
LIBRECHAT_CODE_GITHUB_PRIVATE_KEY_FILE: '/does/not/matter',
LIBRECHAT_CODE_GITHUB_HOST: 'github.example.test',
LIBRECHAT_CODE_GITHUB_API_URL: 'https://other.example.test/api/v3',
},
},
);

assert.notEqual(result.status, 0);
assert.match(
result.stderr,
/LIBRECHAT_CODE_GITHUB_HOST must match the GitHub App API hostname/,
);
});

test('CLI validates GitHub App credentials before worker registration', () => {
const result = spawnSync(
process.execPath,
[fileURLToPath(new URL('./cli.js', import.meta.url))],
{
encoding: 'utf8',
env: {
...process.env,
LIBRECHAT_CODE_URL: 'http://127.0.0.1:1/v1',
LIBRECHAT_CODE_WORKER_TOKEN: 'worker-secret',
LIBRECHAT_CODE_WORKER_ID: 'engineering-vm',
LIBRECHAT_CODE_WORKER_DIR: process.cwd(),
LIBRECHAT_CODE_ALLOW_WORKSPACE_COMMANDS: 'true',
LIBRECHAT_CODE_GITHUB_APP_ID: '123',
LIBRECHAT_CODE_GITHUB_INSTALLATION_ID: '456',
LIBRECHAT_CODE_GITHUB_PRIVATE_KEY_FILE: '/does/not/exist/app.pem',
LIBRECHAT_CODE_GITHUB_HOST: undefined,
LIBRECHAT_CODE_GITHUB_API_URL: undefined,
},
},
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /ENOENT|no such file/i);
assert.doesNotMatch(result.stderr, /fetch failed/);
});

test('CLI requires a runtime image for Docker supervision', () => {
const result = spawnSync(
process.execPath,
Expand Down Expand Up @@ -132,7 +231,10 @@ test('CLI requires the macOS NsJail seccomp profile', () => {
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /LIBRECHAT_CODE_DOCKER_SECCOMP_PROFILE is required/);
assert.match(
result.stderr,
/LIBRECHAT_CODE_DOCKER_SECCOMP_PROFILE is required/,
);
});

test('CLI requires a package mount for the macOS NsJail profile', () => {
Expand All @@ -154,7 +256,10 @@ test('CLI requires a package mount for the macOS NsJail profile', () => {
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /LIBRECHAT_CODE_DOCKER_PACKAGES_PATH is required/);
assert.match(
result.stderr,
/LIBRECHAT_CODE_DOCKER_PACKAGES_PATH is required/,
);
});

test('CLI reset does not require Docker runtime launch inputs', () => {
Expand All @@ -176,6 +281,7 @@ test('CLI reset does not require Docker runtime launch inputs', () => {
LIBRECHAT_CODE_RUNTIME_IMAGE: undefined,
LIBRECHAT_CODE_DOCKER_SECCOMP_PROFILE: undefined,
LIBRECHAT_CODE_DOCKER_PACKAGES_PATH: undefined,
LIBRECHAT_CODE_GITHUB_TOKEN: 'github_pat_abcdefghijklmnopqrstuvwxyz',
},
},
);
Expand All @@ -185,6 +291,10 @@ test('CLI reset does not require Docker runtime launch inputs', () => {
result.stderr,
/LIBRECHAT_CODE_(?:RUNTIME_IMAGE|DOCKER_SECCOMP_PROFILE|DOCKER_PACKAGES_PATH) is required/,
);
assert.doesNotMatch(
result.stderr,
/GitHub authentication requires workspace commands/,
);
});

test('CLI relay requires a fixed upstream URL', () => {
Expand Down
Loading