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
13 changes: 10 additions & 3 deletions docs/adr/001-stateful-code-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,23 @@ worker replacement; the UI and operator documentation must not imply otherwise.
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.
- Sandbox isolation and default-deny egress remain the mandatory default;
pairing secures the transport identity but does not make the host a sandbox.
An operator may explicitly delegate network and local-socket restrictions to
an approved outer VM boundary through a named, digested worker policy. That
delegation retains direct workspace filesystem rules, cancellation, and
resource limits. The operator is responsible for preventing permitted host
services (for example, a privileged container socket) from bypassing those
rules and exposing worker identity or credential material.
- 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.
worker CLI, including machine-local execution-policy presets; 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.
Expand Down
35 changes: 35 additions & 0 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,41 @@ LIBRECHAT_CODE_COMMAND_SANDBOX=native-srt librechat-code run \
--worker-dir /path/to/project --allow-workspace-commands
```

### Trusted VM command policy

The native SRT backend can be made intentionally permissive when the selected
machine already supplies an administrator-approved outer security boundary.
The `trusted-vm` preset keeps SRT's direct filesystem rules, credential
masking, private scratch storage, cancellation, time limits, and output limits,
while allowing unmatched outbound destinations, local port binding, and Unix
sockets:

```bash
librechat-code run \
--worker-dir /home/ubuntu/src \
--allow-workspace-writes \
--allow-workspace-commands \
--command-policy-preset trusted-vm
```

`LIBRECHAT_CODE_COMMAND_POLICY_PRESET=trusted-vm` is the environment equivalent.
The default is `restricted`, which preserves the default-deny network policy.
The preset configures `native-srt`; it is not an unsandboxed host-shell
backend. It is rejected unless native workspace commands are enabled. Its
normalized effective controls are included in the worker policy digest, and
the worker advertises `anthropic-srt:trusted-vm` unless an operator supplied a
custom sandbox profile label.

Treat this preset as delegation to the machine's outer security controls. Any
outbound destination can receive workspace data, local listeners can accept
connections reachable under host policy, and Unix socket access may expose
powerful host services such as a container daemon. A socket that grants host
privilege can bypass SRT's filesystem rules and reach worker or GitHub identity
material; the outer VM boundary must prevent that path or explicitly accept
that trust. Register only the intended source root. Worker identity,
mutation-quarantine state, and configured GitHub App key files must remain
outside it.

## Docker runtime supervisor (optional hardened adapter)

`DockerRuntimeSupervisor` is the first self-contained local OCI adapter. It
Expand Down
4 changes: 4 additions & 0 deletions packages/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"types": "./dist/native-sandbox.d.ts",
"import": "./dist/native-sandbox.js"
},
"./native-policy": {
"types": "./dist/native-policy.d.ts",
"import": "./dist/native-policy.js"
},
"./github": {
"types": "./dist/github.d.ts",
"import": "./dist/github.js"
Expand Down
40 changes: 40 additions & 0 deletions packages/code/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,46 @@ test('CLI rejects an unknown command sandbox before entering the run loop', () =
);
});

test('CLI rejects an unknown native SRT command policy preset', () => {
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_COMMAND_POLICY_PRESET: 'host-shell',
},
},
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /must be restricted or trusted-vm/);
});

test('CLI refuses a permissive policy when native commands are unavailable', () => {
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_COMMAND_POLICY_PRESET: 'trusted-vm',
},
},
);

assert.notEqual(result.status, 0);
assert.match(result.stderr, /requires native-srt workspace commands/);
});

test('CLI rejects incomplete GitHub App authentication before worker registration', () => {
const result = spawnSync(
process.execPath,
Expand Down
24 changes: 22 additions & 2 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import {
import { RuntimeWorkspaceCommandSandbox } from './workspace-runtime.js';
import { NativeProcessWorkspaceCommandSandbox } from './native-process.js';
import { NativeWorkspaceCommandPool } from './native-pool.js';
import {
resolveNativeSrtCommandPolicy,
serializeNativeSrtCommandPolicy,
} from './native-policy.js';
import { workspaceMutationGuard } from './workspace-guards.js';
import type { NativeProcessSandboxOptions } from './native-process.js';
import type { LocalWorkspaceConfig } from './workspace.js';
Expand Down Expand Up @@ -404,6 +408,19 @@ async function run(
'LIBRECHAT_CODE_COMMAND_SANDBOX must be native-srt or runtime',
);
}
const commandPolicy = resolveNativeSrtCommandPolicy(
option(args, '--command-policy-preset') ??
process.env.LIBRECHAT_CODE_COMMAND_POLICY_PRESET?.trim().toLowerCase() ??
'restricted',
);
if (
commandPolicy.preset !== 'restricted' &&
(!allowWorkspaceCommands || commandSandboxMode !== 'native-srt')
) {
throw new Error(
'A permissive command policy preset requires native-srt workspace commands',
);
}
const github =
runtimeSessionId == null
? githubCredentials()
Expand Down Expand Up @@ -756,6 +773,7 @@ async function run(
});
const nativeOptions: NativeProcessSandboxOptions = {
workspaceRoot: canonicalWorkerDirectory!,
commandPolicy,
protectedPaths: [
identityPath,
...rootQuarantinePaths.values(),
Expand Down Expand Up @@ -820,7 +838,9 @@ async function run(
sandboxProfile:
process.env.LIBRECHAT_CODE_SANDBOX_PROFILE ??
(allowWorkspaceCommands && commandSandboxMode === 'native-srt'
? 'anthropic-srt'
? commandPolicy.preset === 'restricted'
? 'anthropic-srt'
: `anthropic-srt:${commandPolicy.preset}`
: runtimeMode.startsWith('docker')
? 'oci-docker'
: 'nsjail'),
Expand All @@ -829,7 +849,7 @@ async function run(
.update(policy)
.update(
allowWorkspaceCommands && commandSandboxMode === 'native-srt'
? `\0native-srt\0${commandAllowedDomains.join('\0')}\0${github.policyIdentity}`
? `\0native-srt\0${serializeNativeSrtCommandPolicy(commandPolicy)}\0${commandAllowedDomains.join('\0')}\0${github.policyIdentity}`
: '',
)
.digest('hex'),
Expand Down
1 change: 1 addition & 0 deletions packages/code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './storage.js';
export * from './runtime.js';
export * from './workspace.js';
export * from './workspace-runtime.js';
export * from './native-policy.js';
export * from './native-sandbox.js';
export * from './native-process.js';
export * from './github.js';
Expand Down
63 changes: 63 additions & 0 deletions packages/code/src/native-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import {
normalizeNativeSrtCommandPolicy,
resolveNativeSrtCommandPolicy,
serializeNativeSrtCommandPolicy,
} from './native-policy.js';

test('restricted remains the default native SRT command policy', () => {
assert.deepEqual(resolveNativeSrtCommandPolicy(), {
version: 1,
preset: 'restricted',
network: {
outbound: 'allowlist',
allowLocalBinding: false,
allowAllUnixSockets: false,
},
});
});

test('trusted-vm resolves to explicit permissive network controls', () => {
assert.deepEqual(resolveNativeSrtCommandPolicy('trusted-vm'), {
version: 1,
preset: 'trusted-vm',
network: {
outbound: 'unrestricted',
allowLocalBinding: true,
allowAllUnixSockets: true,
},
});
});

test('unknown and forged native policies fail closed', () => {
assert.throws(
() => resolveNativeSrtCommandPolicy('host-shell'),
/must be restricted or trusted-vm/,
);
assert.throws(
() =>
normalizeNativeSrtCommandPolicy({
...resolveNativeSrtCommandPolicy('restricted'),
network: {
...resolveNativeSrtCommandPolicy('restricted').network,
allowAllUnixSockets: true,
},
}),
/does not match its preset/,
);
});

test('serialized policy is stable and includes effective controls', () => {
const first = serializeNativeSrtCommandPolicy(
resolveNativeSrtCommandPolicy('trusted-vm'),
);
const second = serializeNativeSrtCommandPolicy(
resolveNativeSrtCommandPolicy('trusted-vm'),
);
assert.equal(first, second);
assert.match(first, /"outbound":"unrestricted"/);
assert.match(first, /"allowLocalBinding":true/);
assert.match(first, /"allowAllUnixSockets":true/);
});
86 changes: 86 additions & 0 deletions packages/code/src/native-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export const NATIVE_SRT_COMMAND_POLICY_PRESETS = [
'restricted',
'trusted-vm',
] as const;

export type NativeSrtCommandPolicyPreset =
typeof NATIVE_SRT_COMMAND_POLICY_PRESETS[number];

export interface NativeSrtCommandPolicy {
version: 1;
preset: NativeSrtCommandPolicyPreset;
network: {
outbound: 'allowlist' | 'unrestricted';
allowLocalBinding: boolean;
allowAllUnixSockets: boolean;
};
}

const PRESETS: Record<NativeSrtCommandPolicyPreset, NativeSrtCommandPolicy> = {
restricted: {
version: 1,
preset: 'restricted',
network: {
outbound: 'allowlist',
allowLocalBinding: false,
allowAllUnixSockets: false,
},
},
'trusted-vm': {
version: 1,
preset: 'trusted-vm',
network: {
outbound: 'unrestricted',
allowLocalBinding: true,
allowAllUnixSockets: true,
},
},
};

function isPreset(value: unknown): value is NativeSrtCommandPolicyPreset {
return (
typeof value === 'string' &&
NATIVE_SRT_COMMAND_POLICY_PRESETS.some((preset) => preset === value)
);
}

/** Resolve a named convenience preset into the explicit policy SRT enforces. */
export function resolveNativeSrtCommandPolicy(
preset: unknown = 'restricted',
): NativeSrtCommandPolicy {
if (!isPreset(preset)) {
throw new Error(
'Native SRT command policy preset must be restricted or trusted-vm',
);
}
const policy = PRESETS[preset];
return { ...policy, network: { ...policy.network } };
}

/** Validate a programmatic policy and return canonical preset-owned values. */
export function normalizeNativeSrtCommandPolicy(
policy?: NativeSrtCommandPolicy,
): NativeSrtCommandPolicy {
const normalized = resolveNativeSrtCommandPolicy(
policy?.preset ?? 'restricted',
);
if (
policy !== undefined &&
(policy.version !== normalized.version ||
policy.network?.outbound !== normalized.network.outbound ||
policy.network?.allowLocalBinding !==
normalized.network.allowLocalBinding ||
policy.network?.allowAllUnixSockets !==
normalized.network.allowAllUnixSockets)
) {
throw new Error('Native SRT command policy does not match its preset');
}
return normalized;
}

/** Stable policy material used in the bridge capability digest. */
export function serializeNativeSrtCommandPolicy(
policy: NativeSrtCommandPolicy,
): string {
return JSON.stringify(normalizeNativeSrtCommandPolicy(policy));
}
30 changes: 30 additions & 0 deletions packages/code/src/native-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,36 @@ test('executor bootstrap excludes bridge credentials and Node injection variable
await sandbox.close();
});

test('executor forwards the resolved command policy without worker credentials', async () => {
const fake = fixture();
const sandbox = new NativeProcessWorkspaceCommandSandbox(
{
workspaceRoot: '/workspace',
commandPolicy: {
version: 1,
preset: 'trusted-vm',
network: {
outbound: 'unrestricted',
allowLocalBinding: true,
allowAllUnixSockets: true,
},
},
},
fake.fork,
);
await sandbox.prepare();
assert.deepEqual(fake.messages[0].options.commandPolicy, {
version: 1,
preset: 'trusted-vm',
network: {
outbound: 'unrestricted',
allowLocalBinding: true,
allowAllUnixSockets: true,
},
});
await sandbox.close();
});

test('executor hands credentials over IPC only for the current command', async () => {
const fake = fixture();
const sandbox = new NativeProcessWorkspaceCommandSandbox(
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 @@ -182,6 +182,7 @@ export class NativeProcessWorkspaceCommandSandbox
child.on('disconnect', lost);
const {
workspaceRoot,
commandPolicy,
protectedPaths,
allowedDomains,
homeDirectory,
Expand All @@ -192,6 +193,7 @@ export class NativeProcessWorkspaceCommandSandbox
{
options: {
workspaceRoot,
commandPolicy,
protectedPaths,
allowedDomains,
homeDirectory,
Expand Down
Loading