Skip to content

Commit dadbf4a

Browse files
koki-developclaude
andcommitted
feat: add default lodash package to Node-TypeScript runtime
Add lodash and its @types/lodash declarations to the pre-installed node_modules and bind-mount it read-only into /sandbox on the run step (in addition to the existing compile-step mount), so compiled JS resolves require('lodash') at run time and tsc type-checks the import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b1f501e commit dadbf4a

7 files changed

Lines changed: 95 additions & 4 deletions

File tree

e2e/tests/runtime/node-typescript.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,38 @@ tests:
498498
status: "OK"
499499
signal: null
500500
duration_ms: "/^[0-9]+$/"
501+
502+
- name: "pre-installed package lodash with types"
503+
requests:
504+
- input:
505+
runtime: node-typescript
506+
files:
507+
- name: index.ts
508+
type: plain
509+
content: |
510+
import _ from "lodash";
511+
512+
// Verify the pre-installed default package is importable, its
513+
// @types/lodash declarations type-check (chunk returns T[][]),
514+
// and it resolves at run time via the node_modules bind mount.
515+
const result: number[][] = _.chunk([1, 2, 3, 4, 5], 2);
516+
console.log(JSON.stringify(result));
517+
output:
518+
status: 200
519+
body:
520+
compile:
521+
stdout: ""
522+
stderr: ""
523+
output: ""
524+
exit_code: 0
525+
status: "OK"
526+
signal: null
527+
duration_ms: "/^[0-9]+$/"
528+
run:
529+
stdout: "[[1,2],[3,4],[5]]\n"
530+
stderr: ""
531+
output: "[[1,2],[3,4],[5]]\n"
532+
exit_code: 0
533+
status: "OK"
534+
signal: null
535+
duration_ms: "/^[0-9]+$/"

e2e/tests/security/filesystem.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,36 @@ tests:
250250
signal: null
251251
duration_ms: "/^[0-9]+$/"
252252

253+
- name: "node-typescript node_modules is read-only"
254+
requests:
255+
- input:
256+
runtime: node-typescript
257+
files:
258+
- name: index.ts
259+
type: plain
260+
content: |
261+
import * as fs from "fs";
262+
fs.writeFileSync("/sandbox/node_modules/lodash/evil.js", "x");
263+
output:
264+
status: 200
265+
body:
266+
compile:
267+
stdout: ""
268+
stderr: ""
269+
output: ""
270+
exit_code: 0
271+
status: "OK"
272+
signal: null
273+
duration_ms: "/^[0-9]+$/"
274+
run:
275+
stdout: ""
276+
stderr: "/^node:fs:\\d+\\n[\\s\\S]*Error: EROFS: read-only file system, open '/sandbox/node_modules/lodash/evil.js'[\\s\\S]*Node\\.js v[\\d.]+\\n$/"
277+
output: "/^node:fs:\\d+\\n[\\s\\S]*Error: EROFS: read-only file system, open '/sandbox/node_modules/lodash/evil.js'[\\s\\S]*Node\\.js v[\\d.]+\\n$/"
278+
exit_code: 1
279+
status: "OK"
280+
signal: null
281+
duration_ms: "/^[0-9]+$/"
282+
253283
- name: "ruby runtime directory is read-only"
254284
requests:
255285
- input:

internal/sandbox/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Core sandbox execution engine, split across three files:
99
- **configs/seccomp.kafel** — Seccomp-BPF syscall filtering policy written in Kafel. Uses a blacklist approach (DEFAULT ALLOW) blocking dangerous syscalls (io_uring, bpf, userfaultfd, mount, ptrace, etc.) as a defense-in-depth layer. Referenced from nsjail.cfg via `seccomp_policy_file` and copied to `/etc/nsjail/seccomp.kafel` in Docker.
1010
- **defaults/go/** — Embedded `go.mod.tmpl` and `go.sum.tmpl` templates applied as default files for Go runtime execution.
1111
- **defaults/node/** — Embedded `package.json` and `package-lock.json` applied as default files for Node runtime execution. Pins the default packages (currently `lodash`) pre-installed at `/mise/node-modules/node_modules` (bind-mounted read-only into `/sandbox/node_modules` during the run step). Regenerate the lockfile with `npm install --package-lock-only`; never edit by hand.
12-
- **defaults/node-typescript/** — Embedded `package.json`, `package-lock.json`, and `tsconfig.json` applied as default files for Node-TypeScript runtime execution.
12+
- **defaults/node-typescript/** — Embedded `package.json`, `package-lock.json`, and `tsconfig.json` applied as default files for Node-TypeScript runtime execution. Beyond `typescript` and `@types/node`, ships the default packages (currently `lodash` plus its `@types/lodash` declarations) pre-installed at `/mise/ts-node-modules/node_modules`, bind-mounted read-only into `/sandbox/node_modules` on both the compile step (so `tsc` resolves the types) and the run step (so the compiled JS resolves `require()`). Regenerate the lockfile with `npm install --package-lock-only`; never edit by hand.
1313
- **defaults/ruby/** — Embedded `Gemfile` and `Gemfile.lock` applied as default files for Ruby runtime execution. The lockfile is generated via Bundler CLI (`bundle install` + `bundle lock --add-platform x86_64-linux aarch64-linux` + `bundle lock --add-checksums`); never edit by hand. The `CHECKSUMS` section pins each gem's SHA-256 so `bundle install` at Docker build time refuses to proceed if a fetched gem differs from the locked hash (supply-chain protection).
1414

1515
Go runtime rejects user-submitted `go.mod`, `go.sum`, and `main` files (HTTP 400) to enforce use of defaults and prevent overwriting the compiled binary.

internal/sandbox/defaults/node-typescript/package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/sandbox/defaults/node-typescript/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{
22
"private": true,
3+
"dependencies": {
4+
"lodash": "4.18.1"
5+
},
36
"devDependencies": {
7+
"@types/lodash": "4.17.24",
48
"@types/node": "24.12.2",
59
"typescript": "5.9.3"
610
}

internal/sandbox/runtime.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,10 @@ func (nodeTypeScriptRuntime) Command(entryFile string) []string {
576576
}
577577

578578
func (nodeTypeScriptRuntime) BindMounts() []BindMount {
579-
return []BindMount{{Src: "/mise/installs/node/current", Dst: "/mise/installs/node/current"}}
579+
return []BindMount{
580+
{Src: "/mise/installs/node/current", Dst: "/mise/installs/node/current"},
581+
{Src: "/mise/ts-node-modules/node_modules", Dst: "/sandbox/node_modules"}, // pre-installed default packages (read-only); the compiled JS resolves require() against this at run time
582+
}
580583
}
581584

582585
func (nodeTypeScriptRuntime) Env() []string {
@@ -590,7 +593,7 @@ func (nodeTypeScriptRuntime) CompileCommand() []string {
590593
func (nodeTypeScriptRuntime) CompileBindMounts() []BindMount {
591594
return []BindMount{
592595
{Src: "/mise/installs/node/current", Dst: "/mise/installs/node/current"},
593-
{Src: "/mise/ts-node-modules/node_modules", Dst: "/sandbox/node_modules"}, // pre-installed typescript and @types/node (read-only)
596+
{Src: "/mise/ts-node-modules/node_modules", Dst: "/sandbox/node_modules"}, // pre-installed typescript, type definitions, and default packages such as lodash (read-only)
594597
}
595598
}
596599

@@ -658,7 +661,8 @@ func (nodeTypeScriptRuntime) Limits() Limits {
658661

659662
// RestrictedFiles prevents users from overriding package.json and
660663
// package-lock.json, which must match the pre-installed node_modules
661-
// bind mount (contains typescript and @types/node).
664+
// bind mount (contains typescript, type definitions, and default
665+
// packages such as lodash).
662666
func (nodeTypeScriptRuntime) RestrictedFiles() []string {
663667
return []string{"package.json", "package-lock.json"}
664668
}

internal/sandbox/sandbox_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ func Test_readDefaultFiles(t *testing.T) {
310310
assert.Equal(t, "package-lock.json", files[0].Name)
311311
assert.Equal(t, "package.json", files[1].Name)
312312
assert.Contains(t, string(files[1].Content), `"@types/node"`)
313+
assert.Contains(t, string(files[1].Content), `"lodash"`)
313314
assert.Equal(t, "tsconfig.json", files[2].Name)
314315
assert.Contains(t, string(files[2].Content), `"skipLibCheck": true`)
315316
})

0 commit comments

Comments
 (0)