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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:

- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 22
node-version: 22.23.4

- name: Compile anonymous-pipe addon
run: |
Expand Down Expand Up @@ -306,7 +306,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: ['22.21.0', '24.16.0']
node-version: ['22.23.4', '24.22.1']
defaults:
run:
working-directory: packages/code
Expand Down Expand Up @@ -343,7 +343,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24.16.0
node-version: 24.22.1
- run: npm ci
- run: npm run build
- name: Selected project root containment tests
Expand All @@ -370,7 +370,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24.16.0
node-version: 24.22.1
- name: Install bubblewrap
run: |
sudo apt-get update -qq
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/extraction.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: 24.16.0
node-version: 24.22.1
cache: npm
cache-dependency-path: packages/extraction/package-lock.json
- run: npm ci --ignore-scripts
Expand Down
6 changes: 3 additions & 3 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ COPY api/src/spec-guard.c /tmp/spec-guard.c
COPY api/src/tool-call-pipe-bridge.c /tmp/tool-call-pipe-bridge.c
COPY api/src/anonymous-pipes.c /tmp/anonymous-pipes.c
COPY api/src/supervisor-policy.c /tmp/supervisor-policy.c
COPY --from=node:22-slim /usr/local/include/node /usr/local/include/node
COPY --from=node:22.23.4-slim /usr/local/include/node /usr/local/include/node
COPY docker/rootfs-setup.c /tmp/rootfs-setup.c
RUN gcc -shared -fPIC -O2 -Wall -Wextra -Werror -I/usr/local/include/node -o /usr/local/lib/anonymous-pipes.node /tmp/anonymous-pipes.c \
&& gcc -O2 -static -Wall -Wextra -Werror -o /usr/local/bin/sandbox-supervisor-policy /tmp/supervisor-policy.c \
Expand All @@ -49,7 +49,7 @@ FROM nsjail-builder AS nsjail-ipc-test
ENV SANDBOX_PIPE_ADDON=/usr/local/lib/anonymous-pipes.node
COPY docker/python-runtime /python-runtime
RUN python3 /python-runtime/install.py
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
COPY --from=node:22.23.4-slim /usr/local/bin/node /usr/local/bin/node
COPY --from=oven/bun:1.4.2-debian /usr/local/bin/bun /usr/local/bin/bun

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

WORKDIR /sandbox_api

Expand Down
1 change: 1 addition & 0 deletions api/src/api/session-inputs.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let baseUrl: string;
const savedSessionWorkspaceEnabled = config.session_workspace_enabled;

beforeAll(async () => {
await fsp.rm(SESSION_INPUT_CACHE_DIR, { recursive: true, force: true });
config.session_workspace_enabled = true;
const app = express();
/* Mirror index.ts: no global JSON parser, router mounted under /api/v2. */
Expand Down
7 changes: 5 additions & 2 deletions api/src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,11 @@ export function ensureNodeModulesSymlink(
if (!nodeModulesPath) return;
const linkPath = path.join(submissionDir, 'node_modules');
try {
fs.lstatSync(linkPath);
return;
if (!fs.lstatSync(linkPath).isSymbolicLink()) return;
const target = path.resolve(submissionDir, fs.readlinkSync(linkPath));
const packageRoot = `${path.resolve(config.packages_directory)}${path.sep}`;
if (!target.startsWith(packageRoot) || target === path.resolve(nodeModulesPath)) return;
fs.unlinkSync(linkPath);
Comment thread
lia-by-librechat[bot] marked this conversation as resolved.
} catch (err) {
if (errorCode(err) !== 'ENOENT') throw err;
}
Expand Down
198 changes: 198 additions & 0 deletions api/src/package-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

const initializer = fs.readFileSync(
path.resolve(__dirname, '../../docker/package-init.sh'),
'utf8'
);

for (const oldRuntimeExists of [false, true]) {
test(`cached PVC refreshes Node and Bun launchers with ${
oldRuntimeExists ? 'retained' : 'removed'
} old runtimes`, () => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'cached-package-init-')
);
try {
const packages = path.join(root, 'pkgs');
const workspace = path.join(root, 'workspace');
const manifest = path.join(root, 'javascript-packages.txt');
fs.mkdirSync(workspace);
fs.writeFileSync(manifest, 'runtime-fixture@1.0.0\n');
const runtimes = [
{
directory: 'node/24.22.1',
binary: 'bin/node',
args: ['hello world', '--version'],
},
{
directory: 'bun/1.4.2',
binary: 'bun',
args: ['run', 'hello world', '--version'],
},
];
for (const runtime of runtimes) {
const destination = path.join(packages, runtime.directory);
const fixture = path.join(
destination,
'node_modules/runtime-fixture'
);
fs.mkdirSync(fixture, { recursive: true });
fs.mkdirSync(
path.dirname(path.join(destination, runtime.binary)),
{ recursive: true }
);
fs.writeFileSync(
path.join(fixture, 'package.json'),
'{"version": "1.0.0"}'
);
fs.writeFileSync(
path.join(destination, '.package-installed'),
'installed'
);
fs.writeFileSync(
path.join(destination, '.env'),
'preserve runtime environment'
);
fs.writeFileSync(
path.join(destination, runtime.binary),
'#!/bin/bash\nprintf "<%s>\\n" "$@"\n',
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(destination, 'run'),
'#!/bin/bash\nexit 99\n',
{ mode: 0o755 }
);
}
const python = path.join(packages, 'python/3.14.4');
for (const name of [
'PIL',
'markitdown',
'chdb',
'statsmodels',
'rasterio',
]) {
fs.mkdirSync(
path.join(python, 'lib/python3.14/site-packages', name),
{ recursive: true }
);
}
fs.writeFileSync(
path.join(python, 'lib/python3.14/.sandbox-pipe-runtime-v1'),
'installed'
);
fs.writeFileSync(
path.join(python, '.package-installed'),
'installed'
);
fs.mkdirSync(path.join(packages, 'bash/5.2.0'), {
recursive: true,
});
fs.writeFileSync(
path.join(packages, 'bash/5.2.0/.package-installed'),
'installed'
);
fs.writeFileSync(
path.join(packages, '.initialized'),
'original initialization'
);

const forbidden = path.join(root, 'forbidden');
fs.mkdirSync(forbidden);
for (const binary of [
'curl',
'wget',
'make',
'npm',
'tar',
'unzip',
]) {
fs.writeFileSync(
path.join(forbidden, binary),
'#!/bin/bash\necho "unexpected reinstall" >&2\nexit 98\n',
{ mode: 0o755 }
);
}
const script = path.join(root, 'package-init.sh');
fs.writeFileSync(
script,
initializer
.split('/pkgs')
.join(packages)
.split('/mnt/data/node_modules')
.join(path.join(workspace, 'node_modules'))
);
const env = {
...process.env,
PATH: `${forbidden}:${process.env.PATH}`,
FORCE_REBUILD: 'false',
PYTHON_VERSION: '3.14.4',
NODE_VERSION: '24.22.1',
BUN_VERSION: '1.4.2',
BASH_PACKAGE_VERSION: '5.2.0',
JS_PACKAGE_MANIFEST: manifest,
};
for (let pass = 0; pass < 2; pass += 1) {
const initialized = Bun.spawnSync({
cmd: ['bash', script],
env,
});
expect(initialized.stderr.toString()).toBe('');
expect(initialized.exitCode).toBe(0);
expect(initialized.stdout.toString()).toContain(
'Packages already initialized'
);
expect(
fs.readFileSync(path.join(packages, '.initialized'), 'utf8')
).toBe('original initialization');
for (const runtime of runtimes) {
const destination = path.join(packages, runtime.directory);
const oldModules = path.join(
packages,
runtime.directory.split('/')[0],
'0.0.1/node_modules'
);
if (oldRuntimeExists)
fs.mkdirSync(oldModules, { recursive: true });
const link = path.join(workspace, 'node_modules');
fs.rmSync(link, { force: true });
fs.symlinkSync(oldModules, link);
const launched = Bun.spawnSync({
cmd: [
path.join(destination, 'run'),
'hello world',
'--version',
],
});
expect(launched.stderr.toString()).toBe('');
expect(launched.exitCode).toBe(0);
expect(launched.stdout.toString()).toBe(
runtime.args.map(arg => `<${arg}>\n`).join('')
);
expect(fs.readlinkSync(link)).toBe(
path.join(destination, 'node_modules')
);
expect(
fs.readFileSync(
path.join(destination, '.package-installed'),
'utf8'
)
).toBe('installed');
expect(
fs.readFileSync(path.join(destination, '.env'), 'utf8')
).toBe('preserve runtime environment');
expect(
fs
.readdirSync(destination)
.some(name => name.startsWith('.run.'))
).toBe(false);
}
}
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
}
Loading
Loading