Skip to content

Commit c93a454

Browse files
🔗 fix: Update Node Runtimes And Preserve Restored Modules (#347)
* 🔗 fix: Refresh Runtime Module Links After Upgrades * fix: Preserve Canonical User Module Links * test: Isolate Input-Cache Route Setup * fix: Complete Runtime Module Link Migration Paths * fix: Update Patched Node Runtimes And Align Rollout Defaults --------- Co-authored-by: Lia <lia@librechat.ai>
1 parent 6c5491c commit c93a454

20 files changed

Lines changed: 616 additions & 54 deletions

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108

109109
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
110110
with:
111-
node-version: 22
111+
node-version: 22.23.4
112112

113113
- name: Compile anonymous-pipe addon
114114
run: |
@@ -306,7 +306,7 @@ jobs:
306306
strategy:
307307
fail-fast: false
308308
matrix:
309-
node-version: ['22.21.0', '24.16.0']
309+
node-version: ['22.23.4', '24.22.1']
310310
defaults:
311311
run:
312312
working-directory: packages/code
@@ -343,7 +343,7 @@ jobs:
343343
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
344344
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
345345
with:
346-
node-version: 24.16.0
346+
node-version: 24.22.1
347347
- run: npm ci
348348
- run: npm run build
349349
- name: Selected project root containment tests
@@ -370,7 +370,7 @@ jobs:
370370
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
371371
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
372372
with:
373-
node-version: 24.16.0
373+
node-version: 24.22.1
374374
- name: Install bubblewrap
375375
run: |
376376
sudo apt-get update -qq

‎.github/workflows/extraction.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
1515
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
1616
with:
17-
node-version: 24.16.0
17+
node-version: 24.22.1
1818
cache: npm
1919
cache-dependency-path: packages/extraction/package-lock.json
2020
- run: npm ci --ignore-scripts

‎api/Dockerfile‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ COPY api/src/spec-guard.c /tmp/spec-guard.c
3535
COPY api/src/tool-call-pipe-bridge.c /tmp/tool-call-pipe-bridge.c
3636
COPY api/src/anonymous-pipes.c /tmp/anonymous-pipes.c
3737
COPY api/src/supervisor-policy.c /tmp/supervisor-policy.c
38-
COPY --from=node:22-slim /usr/local/include/node /usr/local/include/node
38+
COPY --from=node:22.23.4-slim /usr/local/include/node /usr/local/include/node
3939
COPY docker/rootfs-setup.c /tmp/rootfs-setup.c
4040
RUN gcc -shared -fPIC -O2 -Wall -Wextra -Werror -I/usr/local/include/node -o /usr/local/lib/anonymous-pipes.node /tmp/anonymous-pipes.c \
4141
&& gcc -O2 -static -Wall -Wextra -Werror -o /usr/local/bin/sandbox-supervisor-policy /tmp/supervisor-policy.c \
@@ -49,7 +49,7 @@ FROM nsjail-builder AS nsjail-ipc-test
4949
ENV SANDBOX_PIPE_ADDON=/usr/local/lib/anonymous-pipes.node
5050
COPY docker/python-runtime /python-runtime
5151
RUN python3 /python-runtime/install.py
52-
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
52+
COPY --from=node:22.23.4-slim /usr/local/bin/node /usr/local/bin/node
5353
COPY --from=oven/bun:1.4.2-debian /usr/local/bin/bun /usr/local/bin/bun
5454

5555
# ============================================================================
@@ -138,7 +138,7 @@ RUN chmod +x /usr/sbin/nsjail
138138
# The per-invocation broker starts Node after restore, so its OpenSSL process
139139
# state is not captured in the MicroVM snapshot. Pulling the Node binary
140140
# is ~30 MB and avoids inheriting npm/node_modules from a full apt install.
141-
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
141+
COPY --from=node:22.23.4-slim /usr/local/bin/node /usr/local/bin/node
142142

143143
WORKDIR /sandbox_api
144144

‎api/src/api/session-inputs.routes.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let baseUrl: string;
2626
const savedSessionWorkspaceEnabled = config.session_workspace_enabled;
2727

2828
beforeAll(async () => {
29+
await fsp.rm(SESSION_INPUT_CACHE_DIR, { recursive: true, force: true });
2930
config.session_workspace_enabled = true;
3031
const app = express();
3132
/* Mirror index.ts: no global JSON parser, router mounted under /api/v2. */

‎api/src/job.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ export function ensureNodeModulesSymlink(
169169
if (!nodeModulesPath) return;
170170
const linkPath = path.join(submissionDir, 'node_modules');
171171
try {
172-
fs.lstatSync(linkPath);
173-
return;
172+
if (!fs.lstatSync(linkPath).isSymbolicLink()) return;
173+
const target = path.resolve(submissionDir, fs.readlinkSync(linkPath));
174+
const packageRoot = `${path.resolve(config.packages_directory)}${path.sep}`;
175+
if (!target.startsWith(packageRoot) || target === path.resolve(nodeModulesPath)) return;
176+
fs.unlinkSync(linkPath);
174177
} catch (err) {
175178
if (errorCode(err) !== 'ENOENT') throw err;
176179
}

‎api/src/package-init.test.ts‎

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { expect, test } from 'bun:test';
2+
import * as fs from 'node:fs';
3+
import * as os from 'node:os';
4+
import * as path from 'node:path';
5+
6+
const initializer = fs.readFileSync(
7+
path.resolve(__dirname, '../../docker/package-init.sh'),
8+
'utf8'
9+
);
10+
11+
for (const oldRuntimeExists of [false, true]) {
12+
test(`cached PVC refreshes Node and Bun launchers with ${
13+
oldRuntimeExists ? 'retained' : 'removed'
14+
} old runtimes`, () => {
15+
const root = fs.mkdtempSync(
16+
path.join(os.tmpdir(), 'cached-package-init-')
17+
);
18+
try {
19+
const packages = path.join(root, 'pkgs');
20+
const workspace = path.join(root, 'workspace');
21+
const manifest = path.join(root, 'javascript-packages.txt');
22+
fs.mkdirSync(workspace);
23+
fs.writeFileSync(manifest, 'runtime-fixture@1.0.0\n');
24+
const runtimes = [
25+
{
26+
directory: 'node/24.22.1',
27+
binary: 'bin/node',
28+
args: ['hello world', '--version'],
29+
},
30+
{
31+
directory: 'bun/1.4.2',
32+
binary: 'bun',
33+
args: ['run', 'hello world', '--version'],
34+
},
35+
];
36+
for (const runtime of runtimes) {
37+
const destination = path.join(packages, runtime.directory);
38+
const fixture = path.join(
39+
destination,
40+
'node_modules/runtime-fixture'
41+
);
42+
fs.mkdirSync(fixture, { recursive: true });
43+
fs.mkdirSync(
44+
path.dirname(path.join(destination, runtime.binary)),
45+
{ recursive: true }
46+
);
47+
fs.writeFileSync(
48+
path.join(fixture, 'package.json'),
49+
'{"version": "1.0.0"}'
50+
);
51+
fs.writeFileSync(
52+
path.join(destination, '.package-installed'),
53+
'installed'
54+
);
55+
fs.writeFileSync(
56+
path.join(destination, '.env'),
57+
'preserve runtime environment'
58+
);
59+
fs.writeFileSync(
60+
path.join(destination, runtime.binary),
61+
'#!/bin/bash\nprintf "<%s>\\n" "$@"\n',
62+
{ mode: 0o755 }
63+
);
64+
fs.writeFileSync(
65+
path.join(destination, 'run'),
66+
'#!/bin/bash\nexit 99\n',
67+
{ mode: 0o755 }
68+
);
69+
}
70+
const python = path.join(packages, 'python/3.14.4');
71+
for (const name of [
72+
'PIL',
73+
'markitdown',
74+
'chdb',
75+
'statsmodels',
76+
'rasterio',
77+
]) {
78+
fs.mkdirSync(
79+
path.join(python, 'lib/python3.14/site-packages', name),
80+
{ recursive: true }
81+
);
82+
}
83+
fs.writeFileSync(
84+
path.join(python, 'lib/python3.14/.sandbox-pipe-runtime-v1'),
85+
'installed'
86+
);
87+
fs.writeFileSync(
88+
path.join(python, '.package-installed'),
89+
'installed'
90+
);
91+
fs.mkdirSync(path.join(packages, 'bash/5.2.0'), {
92+
recursive: true,
93+
});
94+
fs.writeFileSync(
95+
path.join(packages, 'bash/5.2.0/.package-installed'),
96+
'installed'
97+
);
98+
fs.writeFileSync(
99+
path.join(packages, '.initialized'),
100+
'original initialization'
101+
);
102+
103+
const forbidden = path.join(root, 'forbidden');
104+
fs.mkdirSync(forbidden);
105+
for (const binary of [
106+
'curl',
107+
'wget',
108+
'make',
109+
'npm',
110+
'tar',
111+
'unzip',
112+
]) {
113+
fs.writeFileSync(
114+
path.join(forbidden, binary),
115+
'#!/bin/bash\necho "unexpected reinstall" >&2\nexit 98\n',
116+
{ mode: 0o755 }
117+
);
118+
}
119+
const script = path.join(root, 'package-init.sh');
120+
fs.writeFileSync(
121+
script,
122+
initializer
123+
.split('/pkgs')
124+
.join(packages)
125+
.split('/mnt/data/node_modules')
126+
.join(path.join(workspace, 'node_modules'))
127+
);
128+
const env = {
129+
...process.env,
130+
PATH: `${forbidden}:${process.env.PATH}`,
131+
FORCE_REBUILD: 'false',
132+
PYTHON_VERSION: '3.14.4',
133+
NODE_VERSION: '24.22.1',
134+
BUN_VERSION: '1.4.2',
135+
BASH_PACKAGE_VERSION: '5.2.0',
136+
JS_PACKAGE_MANIFEST: manifest,
137+
};
138+
for (let pass = 0; pass < 2; pass += 1) {
139+
const initialized = Bun.spawnSync({
140+
cmd: ['bash', script],
141+
env,
142+
});
143+
expect(initialized.stderr.toString()).toBe('');
144+
expect(initialized.exitCode).toBe(0);
145+
expect(initialized.stdout.toString()).toContain(
146+
'Packages already initialized'
147+
);
148+
expect(
149+
fs.readFileSync(path.join(packages, '.initialized'), 'utf8')
150+
).toBe('original initialization');
151+
for (const runtime of runtimes) {
152+
const destination = path.join(packages, runtime.directory);
153+
const oldModules = path.join(
154+
packages,
155+
runtime.directory.split('/')[0],
156+
'0.0.1/node_modules'
157+
);
158+
if (oldRuntimeExists)
159+
fs.mkdirSync(oldModules, { recursive: true });
160+
const link = path.join(workspace, 'node_modules');
161+
fs.rmSync(link, { force: true });
162+
fs.symlinkSync(oldModules, link);
163+
const launched = Bun.spawnSync({
164+
cmd: [
165+
path.join(destination, 'run'),
166+
'hello world',
167+
'--version',
168+
],
169+
});
170+
expect(launched.stderr.toString()).toBe('');
171+
expect(launched.exitCode).toBe(0);
172+
expect(launched.stdout.toString()).toBe(
173+
runtime.args.map(arg => `<${arg}>\n`).join('')
174+
);
175+
expect(fs.readlinkSync(link)).toBe(
176+
path.join(destination, 'node_modules')
177+
);
178+
expect(
179+
fs.readFileSync(
180+
path.join(destination, '.package-installed'),
181+
'utf8'
182+
)
183+
).toBe('installed');
184+
expect(
185+
fs.readFileSync(path.join(destination, '.env'), 'utf8')
186+
).toBe('preserve runtime environment');
187+
expect(
188+
fs
189+
.readdirSync(destination)
190+
.some(name => name.startsWith('.run.'))
191+
).toBe(false);
192+
}
193+
}
194+
} finally {
195+
fs.rmSync(root, { recursive: true, force: true });
196+
}
197+
});
198+
}

0 commit comments

Comments
 (0)