Repository navigation
ci: run the package as an embedder ships it, on musl and on macOS 26 - #592
Open
ronleizrowice-ant wants to merge 21 commits into
Open
ronleizrowice-ant wants to merge 21 commits into
ronleizrowice-ant wants to merge 21 commits into
Conversation
… hand @types/node goes from ^18 to ^22 and @types/bun from ^1.3.2 to ^1.4.2, which is what CI runs. Two things surfaced. The CONNECT handler's `head` is reassigned with bytes read off the socket, so it is annotated as a plain Buffer. And the Bun type package redeclares `connect` for 'node:tls' in a way that, against @types/node 22, takes and returns an untyped value, so the four call sites pin their options with `satisfies ConnectionOptions`, and a guard in parent-proxy.test.ts fails the typecheck if ConnectionOptions ever stops being a real type. Adds `npm run test:node`, which runs the two checks that start the library under node rather than Bun. The emitted JavaScript changes only by comment lines.
Linux legs install ripgrep 14.1.1 from the upstream release by sha256 instead of taking whatever apt serves, and run the suites that exercise the scan a second time against 15.2.0: the two differ in exit status for an unreadable directory at --max-depth. The main legs move to Node 22 and run `npm run test:node` on Node 20, 22 and 24; publish moves from Node 18, below the package's floor, to 22. `npm install` becomes `npm ci`. Actions are pinned by commit with the version in a trailing comment.
Node 20 is past its end of life, and nothing here is built or tested on it first: the legs build on 22. engines becomes >=22, the steps that ran the node-side checks on Node 20 go, and the package verification in release.yml covers 22 and 24. An install on Node 20 with engine-strict set is now refused, where it used to go through.
The suites run the sources unbundled. An application that embeds the library ships it in one file, minified and compiled to bytecode, where a function's name, its source text and the files beside it on disk are all gone, and nothing here would notice code that leans on one of them. `npm run test:bundled` compiles a small program that imports dist/ that way and runs it: it initialises the manager, wraps real commands, and checks on the host what they wrote, that the network was closed, that the command's own exit status came back and that the mount point for a denied path is gone afterwards. It imports the built package, so it has a tsconfig of its own that is checked after the build rather than with the rest of test/.
Three things CI did not see. The package as an embedder ships it, which `npm run test:bundled` now runs on every leg that can wrap. musl: the container job repeats its two runs in alpine:3.22, with the seccomp helper built static against musl's libseccomp and Alpine's own bubblewrap, socat and ripgrep. And the newest macOS beside the two images the macOS legs name, since what Seatbelt and the profile's regex engine accept has moved between releases.
An embedding application is built into code-split ESM chunks as often as into one flat file, and a split bundle evaluates modules in another order with the library in a chunk of its own. The runner now builds the program both ways, and the program reaches the library through import() so that it really does sit in a second chunk. Each executable is copied, with the seccomp helper, into an empty directory and run from there. An embedded copy has no vendor/, no node_modules and no package.json beside it, so a lookup that starts from the library's own location has to find nothing and not matter. On Linux the program also provokes a refusal, a profile with more deny paths than bubblewrap takes arguments for, and checks that the error is still an instance of the exported class and still carries the name and the code a caller branches on, with every identifier shortened. Inside a container the program asks for the weaker nested sandbox, as the container suite does: bubblewrap cannot mount /proc there.
Both container steps piped bun's installer into a shell. They now fetch the 1.4.2 release archive for the architecture, glibc or musl, check it against a sha256 recorded here and install the one binary, the way the ripgrep steps do.
…r names An application that has the seccomp helper and ripgrep compiled into its own executable reaches them by running itself under another name: `seccomp.argv0` with `applyPath: '/proc/self/fd/3'`, fd 3 of every wrapped command being an open handle on the executable, and `ripgrep.argv0` with the executable as the command. No suite runs that arrangement, and it is a path of its own through the wrap: the ARGV0 assignment ahead of the helper, a helper path that is used as given and never looked for, and no helper file anywhere to find. The smoke program now plays such an executable. Started as `apply-seccomp` or as `rg` it runs the real one; and on Linux the runner runs each build a second time configured that way, checking that the scan did start it as `rg`, that the wrapped command did reach the helper through fd 3, and that it ran under the seccomp filter.
This was referenced Sep 28, 2026
Closed
ronleizrowice-ant
pushed a commit
that referenced
this pull request
Oct 2, 2026
…tarts
`srt` built into a single file with `bun build --compile` exits before
it parses any argument:
Fatal error: error: Cannot find module '../package.json' from
'/$bunfs/root/srt-bun'
#571 made `getPackageVersion` load the manifest with
`createRequire(import.meta.url)('../package.json')`, which is resolved
at run time. From `dist/cli.js` that finds the package's own manifest,
but a compiled binary's module lives in Bun's virtual filesystem and
has no manifest beside it, so the require throws. The CLI calls
`getPackageVersion` while it builds the commander program, so every
invocation fails, not only `--version`.
#592's new bundled test compiles a program that imports `dist/` with
`bun build --compile`, which is the library path embedders use. It
never compiles `src/cli.ts`, so it does not reach this.
A static JSON import (`with { type: 'json' }`) resolves the same file
for `tsc`'s output, where `dist/cli.js` still reads `../package.json`
at load time as before, and lets Bun embed the manifest in a compiled
binary. The unused `createRequire` import goes, and the comment says
why the import is static. The no-fallback rule the comment states is
unchanged: a missing manifest still fails rather than printing a
made-up version.
Checked on macOS:
- `npm run build`, `npm run typecheck` and `eslint src/cli.ts` pass
- `node dist/cli.js --version` prints 0.0.77
- `bun build --compile --minify src/cli.ts` gives a binary that prints
0.0.77 and sandboxes as expected: a write to the home folder is
refused, the network is closed without an allowlist, and with
`allowedDomains: ["example.com"]` only that host is reachable
- `bun test` gives the same results with and without the change here
(1201 pass, 4 fail both ways: three need ripgrep installed, one is a
3 s timing test)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml is main's: it was rewritten there (pack, verify, publish what was packed), it already runs the package on the engines floor, and nothing of this branch's changes to it is carried. `engines` is main's >=22.12.0. integration-tests.yml keeps both sets of steps: bubblewrap 0.4.1 from main, then ripgrep 15.2.0 and the Node 22 and 24 steps from here. The Docker legs keep main's bound-in Node 22. Joins that no conflict marker shows: - With @types/node 22 and @types/bun 1.4.2 together, connect() of node:tls is any, so seventeen callback parameters in six test files that are new on main were implicitly any. They are annotated. - FileSink.write() may return a promise under those typings, which openFdStreams() left floating. It is chained before flush(), so that its failure reaches the stream's callback. - The list of suites rerun on ripgrep 15.2.0 says it is every suite that names ripgrep or the scan, less those that do not depend on it. Six such suites are new on main: linux-file-above-deny and literal-path-entries depend on the scan and join the list, the other four are named as left out. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
One block, in the Ubuntu container step. node and npm are the runner's, bound in, as on main: the image has Node 18, below what `engines` allows. bun comes from its release archive by checksum, as here, so the piped installer and its PATH entry go. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Apart from test:node, which the toolchain pins add: two lines added side by side would conflict once those are squashed onto main. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
ronleizrowice-ant
enabled auto-merge (squash)
October 20, 2026 06:12
This was referenced Oct 20, 2026
ronleizrowice-ant
disabled auto-merge
October 20, 2026 09:56
- A failed write to an inherited fd has a test. Under Bun a chunk too large to buffer makes write() itself reject, which main leaves floating and reports to the stream as a success. The three ways a write fails now go through one async function. - tls.connect() is typed once, in test/helpers/tls-connect.ts, in place of an annotation at each callback. All eighteen suites that dial TLS use it, so their options and sockets are checked again. The guard on ConnectionOptions moves there. - The two ripgrep installs share .github/scripts/install-ripgrep.sh. - `bun test` passes over an argument that matches no file, so the step that reruns thirteen suites on ripgrep 15.2.0 first checks that each exists. - test:node also runs the four other suites that start `node` themselves (real-path, default-write-paths, rules-through-links-on-every-runtime, tls-terminate-tunnel-ends), which so far only saw Node 22. - The Node 22 step goes back to what it is on main: `npm test` has already run the rest with Node 22 on PATH. The comment no longer promises every release line. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
The smoke test: - On Linux a missing helper or ripgrep fails the run. It used to drop the seccomp check and both multicall modes, and exit 0. - Two checks could not fail. The dependency check came after an initialize() that throws for every error: it now reads the warning a helper that is not found gives. "The network is closed" connected to an outside address, so a host with no route passed: it now connects to a listener on the host, which the same words reach from outside the sandbox. - A failed check throws, so the clean-up runs. A program that cannot be started is reported with its reason. The hint that dist/ is not built could not be reached, and goes. - An ARGV0 or SRT_SMOKE_* inherited from the caller's shell no longer decides what the program runs as. Host paths are quoted in the wrapped command. The workflow: - The Alpine leg runs the seccomp helper the step before it built, static against glibc: that is the file the npm package brings to a musl host. It used to build another against musl, which nobody ships. - Lint runs after the build. Without dist/ the rules that read types saw `any` throughout test/smoke. - The two installs of bun share .github/scripts/install-bun.sh, which retries. The Alpine script has pipefail for its `tar | tar`. - macOS 26 is a `label` of its leg, so `matrix.arch` stays an architecture. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
- tls.connect() is untyped because the Bun type package redeclares it in 'node:tls' with three names that @types/node 22 declares in 'tls' alone. src/node-tls.d.ts gives the module those names. The helper, the import lines in eighteen suites and the `satisfies` in parent-proxy.ts all go: a misspelt option is refused and the result is a TLSSocket wherever it is imported, in new code too. Should the file stop working, seven suites fail the type check. - ripgrep 15.2.0 goes on PATH for the second full run, the one on bubblewrap 0.12.0, in place of a list of thirteen suites kept by hand. Every suite now sees it, a new one included, and the step, its list and the check that each name exists go. That run varies two tools at once. - One step installs both releases, so the script is a function in it. - Comments are cut to what the next reader needs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
- The smoke test is a suite like any other, test/smoke/bundled.test.ts, as tls-terminate-tunnel-ends already is for its fixture. The runner, the `test:bundled` script and the CI step go, and the second full run on Linux (bubblewrap 0.12.0, ripgrep 15.2.0) now covers it too. - It needed a tsconfig, an eslint block and an exclude of its own for one reason: the type check and lint ran before dist/ was built. `npm run typecheck` now builds src instead of only checking it, and lint follows it. All three go. - embedder.ts: one configuration literal instead of two, paths relative to the working directory instead of quoted, the multicall part in fourteen lines, one header instead of two that overlapped. Every check stays. - The Ubuntu container takes the runner's bun, bound in like node. That leaves Alpine as the only install from an archive, so the script is inline there. - The Alpine step uses the workspace as the step before left it. What is in node_modules and dist is JavaScript, so the copy, the second `npm ci` and the second build go, about 70 s a leg. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
…s built once On the machine that built the bundle the library still finds the checkout's helper, so a bundle that lost the setting passed. The type check already builds dist/, so the build step after it goes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FydxqUESqxwSCVFnhk3gfv Co-authored-by: ronleizrowice <ronleizrowice@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Human: @ronleizrowice-ant
Requested by Ron · project thread
CI now also runs the built package the way an embedder ships it (bundled, minified, as bytecode), on musl, and on macOS 26. It covered only the unbundled sources, on glibc Linux and two pinned macOS images.
On top of #589 it is +365 −22 in 5 files.
What changes
test/smoke/bundled.test.tsbuildstest/smoke/embedder.ts, a program that importsdist/, withbun build --compile --minify --bytecode, as one file and as split ESM chunks, and runs each from an empty directory. The program wraps a real command, checks what it did on the host and, on Linux, that a refusal keeps its class,nameandcode.seccomp.argv0,ripgrep.argv0), a path no suite ran.npm test, so every Linux and macOS leg runs it, on Linux with both pairs of bubblewrap and ripgrep.alpine:3.22container step: the two uid 0 runs and the new suite, on musl. It uses the workspace as the Ubuntu step left it, with the seccomp helper that step built, static against glibc, because that is the file the npm package brings to a musl host. bun is the musl build, from the release archive by sha256.macos-26arm64 leg, told apart by alabel.npm run typecheckbuildssrcinstead of only checking it, and lint follows it, becausetest/smokeimportsdist/.Who sees a difference
npm run typechecknow writesdist/.Known limits
test/docker-weak-sandbox.test.tsand the new suite only. It is a second step of the Ubuntu job, whose helper,node_modulesanddistit uses, so a failure there skips it.alpine:3.22andubuntu:24.04are tags, and the packages installed inside them float.Merge order
Stacked on #589: merge that first, squashed. This branch then conflicts with
mainin two places where the two PRs change neighbouring lines (package.json, the step afternpm ci).maingets merged in here at that point, with the tree unchanged, so an approval is best given after that.main merged in on 2026-10-20
Through #589's branch.
main merged in on 2026-11-06
mainat27fce0dis merged in at234678f, without a conflict. The new certificate steps run first in both jobs, so the Alpine step comes after them like the Ubuntu one, and the macOS arm64 leg is onmacos-15beside the newmacos-26one.4f3a8ddand503075eride along, with three small things the second review had left as optional:dist/, and is now called "Build and typecheck".test/docker-weak-sandbox.test.tsnames the Alpine run.Review
main, at54716d5(2026-10-20): seventeen findings for what this PR adds to ci: pin ripgrep and the type packages, enforce the lockfile, and run the Node tests on 22 and 24 #589. No fault on the normal path, several checks that could not fail. Answered inbc03da8: a missing helper or ripgrep fails the run, the dependency check reads the warning a missing helper gives, "the network is closed" has a listener on the host as its control, a failed check cleans up, Alpine runs the helper that ships.bc03da8, applied inadbae33. The runner, its script, its CI step, a tsconfig, an eslint block and an install script go. This PR's own part was +493 −10 in 7 files before.Testing
bc03da8, fails the same 24 names asmainon the same machine, all for want of an outside network.main merged in again on 2026-11-09
mainat951e641is merged in atdd943e9, without a conflict. What this pull request changes is the same as before.Claude session #01FydxqU
Generated by Claude Code