From a8498d82852da3437a42b0021fe984cca4e8eb3e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 14 Sep 2026 19:41:57 -0400 Subject: [PATCH 1/3] fix: Retain Quarantine After Failed Environment Setup --- packages/code/README.md | 7 ++--- packages/code/src/cli.ts | 4 +-- packages/code/src/environment-live.test.ts | 30 +++++++++++++++++----- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/code/README.md b/packages/code/README.md index c3bb16d3..b3d3cc88 100644 --- a/packages/code/README.md +++ b/packages/code/README.md @@ -689,9 +689,10 @@ 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 -minutes and captured output to 8 KiB. Setup failure prevents registration. A crash -or uncertain termination retains the existing workspace quarantine marker; inspect -the workspace before clearing quarantine. No setup output is sent to the model. +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 clearing quarantine with `--reset-workspace-quarantine`. +Only successful setup clears its marker. No setup output is sent to the model. Named actions are fixed commands without model-supplied substitution. The bridge advertises only their names and the definition fingerprint, never their shell source diff --git a/packages/code/src/cli.ts b/packages/code/src/cli.ts index 5498af63..134f29e8 100644 --- a/packages/code/src/cli.ts +++ b/packages/code/src/cli.ts @@ -1012,12 +1012,12 @@ async function run( }, controller.signal, ); - await guard.clear('setup'); if (result.exitCode !== 0 || result.timedOut) { throw new Error( - `Environment ${id} setup failed; inspect the setup command before restarting`, + `Environment ${id} setup failed; inspect the workspace and clear its quarantine before restarting`, ); } + await guard.clear('setup'); process.stdout.write( `librechat-code: environment ${id} prepared\n`, ); diff --git a/packages/code/src/environment-live.test.ts b/packages/code/src/environment-live.test.ts index bb84c8b5..368a86dd 100644 --- a/packages/code/src/environment-live.test.ts +++ b/packages/code/src/environment-live.test.ts @@ -8,13 +8,14 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; -for (const { succeeds, reset } of [ - { succeeds: true, reset: false }, - { succeeds: false, reset: false }, - { succeeds: true, reset: true }, +for (const { succeeds, reset, timesOut } of [ + { succeeds: true, reset: false, timesOut: false }, + { succeeds: false, reset: false, timesOut: false }, + { succeeds: false, reset: false, timesOut: true }, + { succeeds: true, reset: true, timesOut: false }, ]) { test( - `real CLI environment setup gates registration (success=${succeeds}, reset=${reset})`, + `real CLI environment setup gates registration (success=${succeeds}, reset=${reset}, timeout=${timesOut})`, { skip: process.env.LIBRECHAT_CODE_LIVE_SRT_TESTS !== '1', timeout: 20_000, @@ -27,7 +28,7 @@ for (const { succeeds, reset } of [ const path = join(directory, 'environment.yaml'); await writeFile( path, - `name: project\nroot: project\nsetup:\n command: 'printf prepared > prepared.txt; exit ${succeeds ? 0 : 2}'\n timeoutMs: 5000\n`, + `name: project\nroot: project\nsetup:\n command: 'printf prepared >> prepared.txt; ${timesOut ? 'sleep 10' : `exit ${succeeds ? 0 : 2}`}'\n timeoutMs: ${timesOut ? 1000 : 5000}\n`, ); let registrations = 0; let receive: (() => void) | undefined; @@ -38,6 +39,7 @@ for (const { succeeds, reset } of [ request.resume(); if (request.url?.endsWith('/register')) { registrations++; + await assert.rejects(readFile(join(directory, 'quarantine.json')), { code: 'ENOENT' }); if (reset) await assert.rejects( readFile(join(root, 'prepared.txt')), @@ -61,7 +63,7 @@ for (const { succeeds, reset } of [ }); const address = server.address(); assert.ok(address && typeof address !== 'string'); - const child = spawn( + const start = () => spawn( process.execPath, [ fileURLToPath(new URL('./cli.js', import.meta.url)), @@ -90,6 +92,7 @@ for (const { succeeds, reset } of [ stdio: ['ignore', 'pipe', 'pipe'], }, ); + const child = start(); const exited = once(child, 'exit'); t.after(() => child.kill('SIGKILL')); let stderr = ''; @@ -111,6 +114,19 @@ for (const { succeeds, reset } of [ assert.notEqual(code, 0); assert.match(stderr, /Environment project setup failed/); assert.equal(registrations, 0); + const marker = await readFile(join(directory, 'quarantine.json'), 'utf8'); + assert.equal(JSON.parse(marker).workspaceId, 'project'); + const before = await readFile(join(root, 'prepared.txt'), 'utf8'); + const retry = start(); + t.after(() => retry.kill('SIGKILL')); + let retryStderr = ''; + retry.stderr.on('data', chunk => { retryStderr += chunk.toString(); }); + const [retryCode] = await once(retry, 'exit'); + assert.notEqual(retryCode, 0); + assert.match(retryStderr, /quarantined/); + assert.equal(await readFile(join(root, 'prepared.txt'), 'utf8'), before); + assert.equal(await readFile(join(directory, 'quarantine.json'), 'utf8'), marker); + assert.equal(registrations, 0); } }, ); From f8ba12629a183057e1b68723c2a94d9201c780c7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 14 Sep 2026 19:42:37 -0400 Subject: [PATCH 2/3] test: Run Native Environment Setup Lifecycle in CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f041e2d6..718f7079 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,10 @@ jobs: - run: npm run build - name: Native ACL and credential lifecycle tests run: node --test dist/macos-storage.test.js dist/private-storage.test.js dist/storage.test.js dist/github.test.js + - name: Native environment setup lifecycle tests + env: + LIBRECHAT_CODE_LIVE_SRT_TESTS: '1' + run: node --test dist/environment-live.test.js lambda-microvm-provisioning: name: Lambda MicroVM Provisioning From 88c776b2b5ba090543c855cfdf34d2a3829f74e0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 14 Sep 2026 19:48:37 -0400 Subject: [PATCH 3/3] fix: Document and Verify Local Setup Quarantine Recovery --- packages/code/README.md | 8 ++++++-- packages/code/src/cli.ts | 2 +- packages/code/src/environment-live.test.ts | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/code/README.md b/packages/code/README.md index b3d3cc88..8b98618f 100644 --- a/packages/code/README.md +++ b/packages/code/README.md @@ -691,8 +691,12 @@ 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 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 clearing quarantine with `--reset-workspace-quarantine`. -Only successful setup clears its marker. No setup output is sent to the model. +inspect the workspace before running `librechat-code clear-workspace-quarantine +--worker-dir --workspace-id ` with the same +deployment and identity configuration. Only use the separate +`--reset-workspace-quarantine ` run option afterward if a server +fence also needs clearing. Only successful setup automatically clears its marker. +No setup output is sent to the model. Named actions are fixed commands without model-supplied substitution. The bridge advertises only their names and the definition fingerprint, never their shell source diff --git a/packages/code/src/cli.ts b/packages/code/src/cli.ts index 134f29e8..00eca576 100644 --- a/packages/code/src/cli.ts +++ b/packages/code/src/cli.ts @@ -1014,7 +1014,7 @@ async function run( ); if (result.exitCode !== 0 || result.timedOut) { throw new Error( - `Environment ${id} setup failed; inspect the workspace and clear its quarantine before restarting`, + `Environment ${id} setup failed; inspect the workspace and use clear-workspace-quarantine with its root and workspace ID before restarting`, ); } await guard.clear('setup'); diff --git a/packages/code/src/environment-live.test.ts b/packages/code/src/environment-live.test.ts index 368a86dd..9d442982 100644 --- a/packages/code/src/environment-live.test.ts +++ b/packages/code/src/environment-live.test.ts @@ -63,10 +63,15 @@ for (const { succeeds, reset, timesOut } of [ }); const address = server.address(); assert.ok(address && typeof address !== 'string'); - const start = () => spawn( + const start = (clear = false) => spawn( process.execPath, [ fileURLToPath(new URL('./cli.js', import.meta.url)), + ...(clear ? [ + 'clear-workspace-quarantine', + '--worker-dir', root, + '--workspace-id', 'project', + ] : [ 'run', '--environment', path, @@ -74,6 +79,7 @@ for (const { succeeds, reset, timesOut } of [ ...(reset ? ['--reset-workspace-quarantine', 'project'] : []), + ]), ], { env: { @@ -127,6 +133,13 @@ for (const { succeeds, reset, timesOut } of [ assert.equal(await readFile(join(root, 'prepared.txt'), 'utf8'), before); assert.equal(await readFile(join(directory, 'quarantine.json'), 'utf8'), marker); assert.equal(registrations, 0); + const recovery = start(true); + t.after(() => recovery.kill('SIGKILL')); + const [recoveryCode] = await once(recovery, 'exit'); + assert.equal(recoveryCode, 0); + await assert.rejects(readFile(join(directory, 'quarantine.json')), { code: 'ENOENT' }); + assert.equal(await readFile(join(root, 'prepared.txt'), 'utf8'), before); + assert.equal(registrations, 0); } }, );