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
19 changes: 16 additions & 3 deletions docs/remote-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ implementation and an allowlist of workspace IDs, preserves per-workspace
operation restrictions, validates bounded results, and treats an unknown
command failure as an uncertain mutation.

Selected attached workspaces on macOS, Linux, and WSL2 can also advertise Bash
Programmatic Tool Calling. Native Windows workers do not advertise Bash PTC.
Code API then runs each replay iteration through the same workspace-scoped
native SRT executor. Source code operates in the selected local root, while
replay metadata, injected skills and attachments, and generated artifacts are
staged in an execution-private data directory and removed after settlement.
Only authorized file references and returned artifacts cross the relay; the
repository is never uploaded to Code API. This capability is advertised only
when native SRT commands and a file-relay upstream are both configured, so
older or partially configured workers continue to fail closed.

Native SRT is the MVP and default command backend on a user's chosen laptop or
VM. It uses Seatbelt on macOS, bubblewrap/seccomp on Linux, and the SRT
restricted-account helper on Windows. It confines writes to the registered
Expand All @@ -157,9 +168,11 @@ credentials, and denies network egress by default. Startup fails closed when
the platform dependencies are unavailable; there is no unsandboxed fallback.
Use `LIBRECHAT_CODE_COMMAND_ALLOWED_DOMAINS` for an explicit comma-separated
egress allowlist.
Linux hosts must provide Bash at `/bin/bash`, `bubblewrap`, `socat`, and
`ripgrep`; macOS uses system facilities. Windows requires SRT's one-time
restricted-account setup.
Linux hosts must provide `bubblewrap`, `socat`, and `ripgrep`; macOS uses
system facilities. Bash Programmatic Tool Calling additionally requires Bash
5.2 or newer and `jq` on `PATH` on macOS, Linux, and WSL2. The worker resolves
the compatible shell from `PATH` rather than assuming `/bin/bash`. Windows
requires SRT's one-time restricted-account setup.

The optional `docker-nsjail` adapter enables a stronger container boundary with
`--allow-workspace-commands` (or
Expand Down
38 changes: 36 additions & 2 deletions packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,48 @@ policy. This matches the personal-machine SRT trust model; use the Docker/NsJail
backend or a dedicated VM boundary when hard teardown of adversarial process
trees is required.

Linux hosts need Bash at `/bin/bash`, `bubblewrap`, `socat`, and `ripgrep`; macOS uses system
facilities. Follow SRT's one-time restricted-account setup when using Windows.
Linux hosts need `bubblewrap`, `socat`, and `ripgrep`; macOS uses system
facilities. Bash Programmatic Tool Calling additionally requires Bash 5.2 or
newer and `jq` on `PATH` on macOS, Linux, and WSL2. The worker resolves that
shell explicitly instead of assuming `/bin/bash`, which remains Bash 3.2 on
many macOS hosts. Follow SRT's one-time restricted-account setup when using Windows.
An operator may allow explicit egress destinations with the comma-separated
`LIBRECHAT_CODE_COMMAND_ALLOWED_DOMAINS` setting. Treat that as a security
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.

When Code API negotiates `bash` programmatic execution for a selected
workspace, the same native SRT executor also supports replay-mode Programmatic
Tool Calling on macOS, Linux, and WSL2 workers. Native Windows does not
advertise this Bash capability. The repository remains the command working directory. Generated
PTC scripts, replay history, skill files, chat attachments, and returned
artifacts use an owner-only per-execution directory under the worker's private
SRT scratch root, exposed to code as `LIBRECHAT_CODE_DATA_DIR`. That directory
is removed after every iteration and is never placed in the repository.

Replay probes run against a disposable copy-on-write snapshot with network and
socket access denied, including under `trusted-vm`. External effects must not
repeat while discovering pending tools. Use registered tools for network-dependent
replay control flow; the final commit pass runs once under the configured policy.
Each probe's SRT proxy session is revoked before restoring the commit policy;
per-command network overrides alone do not restrict SRT's session-level proxies.
Probe failures do not quarantine the real workspace. Once the commit pass starts,
its fence remains until result restoration succeeds; uncertain finalization
quarantines only that workspace.

Reference inputs and artifact outputs travel only through the configured
`LIBRECHAT_CODE_FILE_RELAY_UPSTREAM`, using Code API's execution-scoped opaque
egress grant. The worker rejects redirects and bounds each transfer to 10 MiB,
each execution to 100 files and 100 MiB total, and transfer concurrency to four.
Caller inputs are limited to 98 files, reserving two for the script and replay
history. Code API reserves one third of the job budget for all transfer batches
and negotiates each transfer's deadline before signing the request.
Its parent process keeps a 64-entry/32-MiB LRU input cache keyed by a stable,
Code-API-authorized digest; sandboxed commands cannot read that cache. Requests
against one workspace remain serialized, while negotiated lease slots allow
different registered roots to execute concurrently.

The native sandbox preserves standard `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`,
and `NO_PROXY` names (including lowercase forms), plus Windows process and profile
variables on Windows. SRT remains responsible for the final sandbox environment
Expand Down
14 changes: 14 additions & 0 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@ async function run(
'LIBRECHAT_CODE_COMMAND_SANDBOX must be native-srt or runtime',
);
}
const nativeProgrammaticEnabled =
allowWorkspaceCommands &&
commandSandboxMode === 'native-srt' &&
process.platform !== 'win32' &&
(fileRelayUpstream?.length ?? 0) > 0;
Comment thread
danny-avila marked this conversation as resolved.
Comment thread
danny-avila marked this conversation as resolved.
const commandPolicy = resolveNativeSrtCommandPolicy(
option(args, '--command-policy-preset') ??
process.env.LIBRECHAT_CODE_COMMAND_POLICY_PRESET?.trim().toLowerCase() ??
Expand Down Expand Up @@ -780,6 +785,9 @@ async function run(
github.privateKeyPath,
].filter((path): path is string => path != null),
allowedDomains: commandAllowedDomains,
...(nativeProgrammaticEnabled
? { programmaticFileUpstream: fileRelayUpstream }
: {}),
...(github.provider
? {
maskedEnvironment: {
Expand Down Expand Up @@ -819,6 +827,9 @@ async function run(
workspaceTools = new SandboxWorkspaceTools({
workspaceTools,
commandWorkspaces: roots.map((root) => root.id),
...(nativeProgrammaticEnabled
? { programmaticLanguages: ['bash'] }
: {}),
commandSandbox:
nativeCommandSandbox ??
new RuntimeWorkspaceCommandSandbox({
Expand Down Expand Up @@ -878,6 +889,9 @@ async function run(
runtimeSupervisor,
capabilities,
workspaceTools,
...(nativeProgrammaticEnabled && nativeCommandSandbox
? { workspaceProgrammatic: nativeCommandSandbox }
: {}),
...(workspaceLeaseSlots > 1 || roots.length > 1
? {
workspaceQuarantines: new Map(
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 @@ -7,6 +7,7 @@ export * from './workspace.js';
export * from './workspace-runtime.js';
export * from './native-policy.js';
export * from './native-sandbox.js';
export * from './native-programmatic.js';
export * from './native-process.js';
export * from './github.js';
export * from './worker.js';
25 changes: 25 additions & 0 deletions packages/code/src/native-pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ const request = (workspaceId: string): WorkspaceExecuteCommandRequest => ({
workspaceId,
command: 'fixture',
});

test('native pool preflights every registered root with bounded concurrency', async () => {
const prepared: string[] = [];
let active = 0;
let peak = 0;
const pool = new NativeWorkspaceCommandPool(roots, 2, (options) => ({
async prepare() {
active += 1;
peak = Math.max(peak, active);
await new Promise(resolve => setTimeout(resolve, 5));
prepared.push(options.workspaceRoot);
active -= 1;
},
async close() {},
async execute() {
throw new Error('unreachable');
},
}));

await pool.prepare();
assert.deepEqual(prepared.sort(), ['/fixture/a', '/fixture/b', '/fixture/c']);
assert.equal(peak, 2);
await pool.close();
});

test('a known-clean executor failure is retired without replaying the command', async () => {
let created = 0;
let executed = 0;
Expand Down
76 changes: 69 additions & 7 deletions packages/code/src/native-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NativeProcessWorkspaceCommandSandbox } from './native-process.js';
import { WorkspaceToolError } from './workspace.js';
import type { NativeProcessSandboxOptions } from './native-process.js';
import type {
BridgeWorkspaceProgrammaticRequest,
WorkspaceExecuteCommandRequest,
WorkspaceExecuteCommandResult,
} from './protocol.js';
Expand All @@ -10,7 +11,10 @@ interface Entry {
sandbox: Pick<
NativeProcessWorkspaceCommandSandbox,
'prepare' | 'execute' | 'close'
>;
> &
Partial<
Pick<NativeProcessWorkspaceCommandSandbox, 'executeProgrammatic'>
>;
busy: boolean;
}

Expand Down Expand Up @@ -92,12 +96,25 @@ export class NativeWorkspaceCommandPool {
}

async prepare(): Promise<void> {
const entry = await this.allocate(this.roots.keys().next().value!);
try {
await entry.sandbox.prepare();
} finally {
entry.busy = false;
}
const workspaceIds = [...this.roots.keys()];
let next = 0;
await Promise.all(
Array.from(
{ length: Math.min(this.capacity, workspaceIds.length) },
async () => {
for (;;) {
const index = next++;
if (index >= workspaceIds.length) return;
const entry = await this.allocate(workspaceIds[index]!);
try {
await entry.sandbox.prepare();
} finally {
entry.busy = false;
}
}
},
),
);
}

async execute(
Expand Down Expand Up @@ -136,6 +153,51 @@ export class NativeWorkspaceCommandPool {
}
}

async executeProgrammatic(
workspaceId: string,
request: BridgeWorkspaceProgrammaticRequest,
signal?: AbortSignal,
): Promise<object> {
const entry = await this.allocate(workspaceId);
let enteredExecutor = false;
try {
if (signal?.aborted)
throw new WorkspaceToolError(
'Programmatic execution cancelled before dispatch',
'EXECUTION_ABORTED',
);
enteredExecutor = true;
if (!entry.sandbox.executeProgrammatic) {
throw new WorkspaceToolError(
'Native programmatic executor is unavailable',
'COMMAND_UNAVAILABLE',
);
}
return await entry.sandbox.executeProgrammatic(
workspaceId,
request,
signal,
);
} catch (error) {
if (
enteredExecutor &&
error instanceof WorkspaceToolError &&
!error.mutationMayHaveCommitted
) {
try {
await entry.sandbox.close();
if (this.entries.get(workspaceId) === entry)
this.entries.delete(workspaceId);
} catch {
/* Retain ownership for subsequent cleanup/shutdown. */
}
}
throw error;
} finally {
entry.busy = false;
}
}

async close(): Promise<void> {
this.closing = true;
await this.allocation;
Expand Down
35 changes: 33 additions & 2 deletions packages/code/src/native-process-child.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { NativeSrtWorkspaceCommandSandbox } from './native-sandbox.js';
import { NativeWorkspaceProgrammaticExecutor } from './native-programmatic.js';
import { WorkspaceToolError } from './workspace.js';
import type { NativeSrtWorkspaceCommandSandboxOptions } from './native-sandbox.js';
import type { WorkspaceExecuteCommandRequest } from './protocol.js';
import type {
BridgeWorkspaceProgrammaticRequest,
WorkspaceExecuteCommandRequest,
} from './protocol.js';

// This entrypoint is private to a forked trusted executor. No HTTP listener,
// argv credentials, bridge token, or persisted pairing material is required.
let sandbox: NativeSrtWorkspaceCommandSandbox | undefined;
let programmaticExecutor: NativeWorkspaceProgrammaticExecutor | undefined;
let active: { id: string; controller: AbortController } | undefined;
let busy = false;
let credentials: Record<string, string> = {};
Expand Down Expand Up @@ -44,11 +49,14 @@ process.on('message', async (raw: unknown) => {
NativeSrtWorkspaceCommandSandboxOptions,
'maskedEnvironment'
> & {
programmaticFileUpstream?: string;
variables?: NonNullable<
NativeSrtWorkspaceCommandSandboxOptions['maskedEnvironment']
>['variables'];
};
request: WorkspaceExecuteCommandRequest;
programmaticRequest?: BridgeWorkspaceProgrammaticRequest;
workspaceId?: string;
credentials?: Record<string, string>;
wrappedCommand?: string;
};
Expand All @@ -62,7 +70,8 @@ process.on('message', async (raw: unknown) => {
try {
let result: unknown;
if (message.type === 'prepare' && !sandbox) {
const { variables, ...options } = message.options;
const { variables, programmaticFileUpstream, ...options } =
message.options;
sandbox = new NativeSrtWorkspaceCommandSandbox({
...options,
...(variables
Expand All @@ -80,11 +89,33 @@ process.on('message', async (raw: unknown) => {
: {}),
});
await sandbox.prepare();
programmaticExecutor = programmaticFileUpstream
? new NativeWorkspaceProgrammaticExecutor({
sandbox,
upstreamUrl: programmaticFileUpstream,
})
: undefined;
await programmaticExecutor?.prepare();
} else if (message.type === 'execute' && sandbox) {
active = { id: message.id, controller: new AbortController() };
credentials = message.credentials ?? {};
wrappedCommand = message.wrappedCommand;
result = await sandbox.execute(message.request, active.controller.signal);
} else if (
message.type === 'programmatic' &&
sandbox &&
programmaticExecutor &&
message.programmaticRequest &&
typeof message.workspaceId === 'string'
) {
active = { id: message.id, controller: new AbortController() };
credentials = message.credentials ?? {};
wrappedCommand = message.wrappedCommand;
result = await programmaticExecutor.execute(
message.programmaticRequest,
message.workspaceId,
active.controller.signal,
);
} else if (message.type === 'close' && sandbox) {
await sandbox.close();
} else throw new Error('Invalid executor state');
Expand Down
Loading