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
5 changes: 4 additions & 1 deletion docs/remote-bridge/worker-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,10 @@ Important semantics:
existing ID such as `primary` to preserve agent/conversation bindings.
- `repo` and `ref` are labels. They do not clone, fetch, or check out anything.
- `root` must already exist. Relative roots resolve from the definition file.
- Setup runs before registration on every worker start. It must be idempotent.
- Setup runs before registration on every worker start by default. It must be
idempotent. Optional `setup.reuse` declares fingerprint inputs and a sandboxed
readiness check to avoid reinstalling an unchanged, still-ready checkout. See
[preparation reuse](../../packages/code/README.md#reusing-a-prepared-checkout).
- A setup failure or timeout prevents registration and leaves a durable
quarantine marker for operator inspection.
- Actions are fixed operator commands. The model selects only the action name
Expand Down
51 changes: 50 additions & 1 deletion packages/code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1035,14 +1035,63 @@ worker runs. This inspection happens at startup, not on the command hot path.

Setup is an operator-authorized startup command under the configured native sandbox
policy. It requires commands to be enabled, runs once per worker startup before
registration, and must be idempotent for restarts. Its timeout is bounded to five
registration by default, and must be idempotent for restarts. Its timeout is bounded to five
minutes and captured output to 8 KiB. Setup failure prevents registration. A nonzero
exit, timeout, crash or uncertain termination retains the workspace quarantine marker;
inspect the workspace before running `librechat-code clear-workspace-quarantine
--worker-dir <environment-root> --workspace-id <environment-name>` with the same
deployment and identity configuration. Only use the separate
`--reset-workspace-quarantine <environment-name>` run option afterward if a server
fence also needs clearing. Only successful setup automatically clears its marker.

## Reusing a prepared checkout

Opt in to checkout-local preparation reuse when setup is an installation rather
than work that must run on every startup:

```yaml
setup:
command: npm ci
timeoutMs: 300000
reuse:
inputs:
- package.json
- package-lock.json
- packages/api/package.json
- packages/data-provider/package.json
checkCommand: test -f node_modules/.package-lock.json && test -x node_modules/.bin/tsc
checkTimeoutMs: 10000
```

Declare **all** relevant manifests, installation configuration and lifecycle-script
inputs. There is no globbing or automatic monorepo discovery. Inputs must be
existing root-confined regular files: at most 32, 8 MiB per file and 32 MiB total.
Include an operator-maintained toolchain/version file if installation uses tools
other than the worker's Node runtime. Lockfile equality alone does not prove that
arbitrary postinstall scripts are reusable.

The worker fingerprints declared file bytes, the setup recipe, checkout inode,
Node version/ABI, platform/architecture and native command policy. A matching
worker-owned receipt runs the readiness check instead of setup. A nonzero check
reruns setup; a timed-out, signalled or aborted check fails without starting a
replacement command. After successful setup, the check must pass and inputs must
remain unchanged before the worker publishes a receipt. Receipts are bounded,
owner-only files alongside the identity, outside all registered roots and denied
to native commands. Deleting one causes setup to run again; it never clears a
quarantine. Checks are operator commands under the same sandbox and mutation guard
as setup, not unsandboxed host scripts. Keep them cheap and non-mutating.

Existing definitions without `reuse` retain the startup behavior. Fresh conversation
instances use the same preparation contract, with independent checkout receipts.
This does not attach another checkout's `node_modules`, provision linked lanes on
command admission, or recheck existing instances on every command. It does not
deduplicate installed dependencies between worktrees or enforce disk quotas.

The next resource-store slice must explicitly grant shared cache paths under SRT,
keep monorepo links and mutable outputs checkout-local, and bound retention. Do not
work around that missing grant by broadening the sandbox root or symlinking another
branch's full installation. Shared download caches alone do not reduce installed
`node_modules` copies.
No setup output is sent to the model.

Named actions are fixed commands without model-supplied substitution. The bridge
Expand Down
68 changes: 45 additions & 23 deletions packages/code/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { createHash, createHmac, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { readdir, realpath, stat } from 'node:fs/promises';
import { basename, join, resolve, relative, isAbsolute, sep } from 'node:path';
import { basename, dirname, join, resolve, relative, isAbsolute, sep } from 'node:path';

import { pairBridgeWorker } from './pairing.js';
import { discoverProjects } from './projects.js';
Expand All @@ -12,6 +12,7 @@ import {
assertEnvironmentDefinitionsOutsideRoots,
EnvironmentWorkspaceTools,
} from './environment.js';
import { prepareCodeEnvironment } from './environment-preparation.js';
import { startFileRelay } from './relay.js';
import { DockerFileRelaySupervisor } from './relay-runtime.js';
import {
Expand All @@ -26,6 +27,7 @@ import {
loadWorkspaceMutationQuarantine,
saveBridgeIdentity,
saveWorkspaceMutationQuarantine,
prepareEnvironmentPreparationDirectory,
} from './storage.js';
import { BridgeWorker } from './worker.js';
import { LocalWorkspaceTools, SandboxWorkspaceTools } from './workspace.js';
Expand Down Expand Up @@ -826,6 +828,17 @@ async function run(
}),
]),
);
const preparationDirectory = join(dirname(identityPath ?? defaultBridgeIdentityPath(workerId)), 'environment-preparation');
if (environments.some(environment => environment.definition.setup?.reuse)) {
const sourceParents = await prepareEnvironmentPreparationDirectory(preparationDirectory);
// Reuse the mount/ancestor isolation checks for this worker-owned state directory.
await assertEnvironmentDefinitionsOutsideRoots([{
path: preparationDirectory, sourceParents,
definition: { name: 'preparation-state', root: preparationDirectory }, fingerprint: '',
}], roots);
}
const preparationReceipt = (root: string) => join(preparationDirectory,
`${createHash('sha256').update(JSON.stringify([codeApiUrl, workerId, root])).digest('hex')}.json`);
// Keep an admission boundary even when trusted-VM checkout routing uses a
// nested repository's remote for the current command.
const admittedGitHubRepositories = github.provider && github.repositoryRouting
Expand Down Expand Up @@ -1048,6 +1061,7 @@ async function run(
commandPolicy,
protectedPaths: [
identityPath,
...(environments.some(environment => environment.definition.setup?.reuse) ? [preparationDirectory] : []),
...environments.map(environment => environment.path),
...rootQuarantinePaths.values(),
github.privateKeyPath,
Expand Down Expand Up @@ -1151,22 +1165,20 @@ async function run(
workspaceIdentity: instance.identity,
workspaceRoot: instance.root,
});
const result = await nativeCommandSandbox.execute(
{
await prepareCodeEnvironment({
root: instance.root, identity: instance.identity, setup,
receiptPath: preparationReceipt(instance.root),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity]),
signal,
execute: (command, timeoutMs) => nativeCommandSandbox.execute({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: id,
command: setup.command,
timeoutMs: setup.timeoutMs,
command,
timeoutMs,
maxOutputBytes: 8192,
},
signal,
);
if (result.exitCode !== 0 || result.timedOut) {
throw new Error(
`Environment ${instance.sourceWorkspaceId} setup failed for its conversation worktree`,
);
}
}, signal),
});
},
discardInstance: async (instance) => {
await nativeCommandSandbox.unregisterRoot(
Expand Down Expand Up @@ -1323,26 +1335,36 @@ async function run(
incarnationId,
);
await guard.assertAvailable();
await guard.arm('Environment setup did not settle', 'setup');
const result = await nativeCommandSandbox.execute(
{
let armed = false;
const preparation = await prepareCodeEnvironment({
root: environment.definition.root,
identity: roots.find(root => root.id === id)!.identity!,
setup, receiptPath: preparationReceipt(environment.definition.root),
context: JSON.stringify([serializeNativeSrtCommandPolicy(commandPolicy), commandAllowedDomains, github.policyIdentity]),
signal: controller.signal,
execute: async (command, timeoutMs) => {
if (!armed) {
await guard.arm('Environment preparation did not settle', 'setup');
armed = true;
}
return nativeCommandSandbox.execute({
protocolVersion: 1,
operation: 'execute_command',
workspaceId: id,
command: setup.command,
timeoutMs: setup.timeoutMs,
command,
timeoutMs,
maxOutputBytes: 8192,
}, controller.signal);
},
controller.signal,
);
if (result.exitCode !== 0 || result.timedOut) {
}).catch(error => {
throw new Error(
`Environment ${id} setup failed; inspect the workspace and use clear-workspace-quarantine with its root and workspace ID before restarting`,
{ cause: error },
);
}
});
await guard.clear('setup');
process.stdout.write(
`librechat-code: environment ${id} prepared\n`,
`librechat-code: environment ${id} ${preparation}\n`,
);
}
} catch (error) {
Expand Down
Loading
Loading